Refactor code structure for improved readability and maintainability

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

View File

@@ -0,0 +1,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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,29 @@
# Server Configuration
PORT=3000
NODE_ENV=development
# Database Configuration
MONGODB_URI=mongodb://localhost:27017/user-management
# JWT Configuration
JWT_SECRET=your-super-secret-jwt-key-here
JWT_EXPIRES_IN=24h
# CORS Configuration
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001
# Logging Configuration
LOG_LEVEL=info
# Rate Limiting Configuration
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100
# Password Configuration
BCRYPT_SALT_ROUNDS=12
# Email Configuration (if implementing email features)
# SMTP_HOST=smtp.gmail.com
# SMTP_PORT=587
# SMTP_USER=your-email@gmail.com
# SMTP_PASS=your-app-password

View File

@@ -0,0 +1,313 @@
# User Management System
A comprehensive user management system built with Node.js, Express, and MongoDB. This project demonstrates enterprise-level patterns for user authentication, authorization, and management.
## Features
### Core Functionality
- **User Registration & Authentication**: Secure user registration with JWT-based authentication
- **Role-Based Access Control (RBAC)**: Admin, User, and Guest roles with permission system
- **Password Security**: BCrypt hashing with configurable salt rounds
- **Account Management**: User activation, deactivation, suspension, and soft deletion
- **Profile Management**: User profile updates with validation
- **Permission System**: Granular permissions for fine-grained access control
### Security Features
- **Rate Limiting**: Configurable rate limits for different endpoints
- **Input Validation**: Comprehensive validation using express-validator
- **Security Headers**: Helmet.js for security headers
- **CORS Protection**: Configurable CORS policies
- **Account Lockout**: Automatic account lockout after failed login attempts
- **JWT Security**: Secure token generation and validation
### API Features
- **RESTful API**: Clean REST API design with proper HTTP methods
- **Pagination**: Efficient pagination for large datasets
- **Search Functionality**: Full-text search across user fields
- **Filtering**: Role-based and status-based filtering
- **Export Functionality**: User data export capabilities
- **Statistics**: User statistics and analytics
### Development Features
- **Error Handling**: Comprehensive error handling with custom error classes
- **Logging**: Structured logging with Winston
- **Documentation**: Detailed API documentation
- **Testing**: Unit and integration tests with Jest
- **Code Quality**: ESLint and Prettier configuration
## Technology Stack
- **Runtime**: Node.js 16+
- **Framework**: Express.js
- **Database**: MongoDB with Mongoose ODM
- **Authentication**: JSON Web Tokens (JWT)
- **Password hashing**: BCrypt
- **Validation**: Joi and express-validator
- **Logging**: Winston
- **Testing**: Jest and Supertest
- **Security**: Helmet, CORS, Rate limiting
## Installation
### Prerequisites
- Node.js (v16 or higher)
- MongoDB (v4.4 or higher)
- npm or yarn
### Setup
1. **Clone the repository**
```bash
git clone <repository-url>
cd user-management
```
2. **Install dependencies**
```bash
npm install
```
3. **Environment configuration**
```bash
cp .env.example .env
```
Update the `.env` file with your configuration:
```env
PORT=3000
NODE_ENV=development
MONGODB_URI=mongodb://localhost:27017/user-management
JWT_SECRET=your-super-secret-jwt-key-here
JWT_EXPIRES_IN=24h
```
4. **Start MongoDB**
```bash
# Using MongoDB service
sudo systemctl start mongod
# Or using Docker
docker run -d -p 27017:27017 --name mongodb mongo:latest
```
5. **Run the application**
```bash
# Development mode
npm run dev
# Production mode
npm start
```
## API Documentation
### Base URL
```
http://localhost:3000/api
```
### Authentication
Most endpoints require authentication. Include the JWT token in the Authorization header:
```
Authorization: Bearer <your-jwt-token>
```
### Endpoints
#### User Management
- `POST /users` - Create new user
- `GET /users` - Get all users (with pagination)
- `GET /users/:id` - Get user by ID
- `PUT /users/:id` - Update user
- `DELETE /users/:id` - Delete user (soft delete)
- `DELETE /users/:id/hard` - Permanently delete user
#### Authentication
- `POST /users/auth` - User login
- `PUT /users/:id/password` - Change password
- `PUT /users/:id/reset-password` - Reset password (admin only)
#### Search & Filtering
- `GET /users/search?q=query` - Search users
- `GET /users/active` - Get active users
- `GET /users/role/:role` - Get users by role
#### Permissions
- `PUT /users/:id/permissions` - Add permission
- `DELETE /users/:id/permissions` - Remove permission
#### Analytics
- `GET /users/stats` - Get user statistics
- `GET /users/export` - Export user data
- `GET /users/:id/activity` - Get user activity
### Example Requests
#### Create User
```bash
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d '{
"username": "john_doe",
"name": "John Doe",
"email": "john@example.com",
"password": "securepassword123",
"age": 30,
"role": "user"
}'
```
#### Authenticate User
```bash
curl -X POST http://localhost:3000/api/users/auth \
-H "Content-Type: application/json" \
-d '{
"username": "john_doe",
"password": "securepassword123"
}'
```
#### Get Users (with authentication)
```bash
curl -X GET http://localhost:3000/api/users \
-H "Authorization: Bearer <your-jwt-token>"
```
## Testing
### Run Tests
```bash
# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage
npm run test:coverage
```
### Test Structure
```
tests/
├── unit/
│ ├── models/
│ ├── services/
│ └── utils/
├── integration/
│ └── routes/
└── setup/
└── testSetup.js
```
## Development
### Code Quality
```bash
# Linting
npm run lint
# Formatting
npm run format
```
### Project Structure
```
src/
├── config/
│ └── database.js
├── middleware/
│ ├── auth.js
│ ├── rateLimiter.js
│ └── validate.js
├── models/
│ └── User.js
├── routes/
│ └── userRoutes.js
├── services/
│ └── UserService.js
├── utils/
│ ├── errors.js
│ └── logger.js
└── server.js
```
### Database Schema
#### User Schema
```javascript
{
id: String, // UUID
username: String, // Unique, 3-20 chars
email: String, // Optional, unique
name: String, // Required, 1-100 chars
age: Number, // Optional, 0-150
password: String, // Hashed, min 8 chars
role: String, // admin, user, guest
status: String, // active, inactive, suspended, deleted
lastLogin: Date, // Last login timestamp
loginAttempts: Number, // Failed login counter
permissions: [String], // Array of permissions
metadata: Object, // Flexible metadata
createdAt: Date, // Auto-generated
updatedAt: Date // Auto-generated
}
```
## Environment Variables
| Variable | Description | Default |
|----------|-------------|-------|
| `PORT` | Server port | 3000 |
| `NODE_ENV` | Environment | development |
| `MONGODB_URI` | MongoDB connection string | mongodb://localhost:27017/user-management |
| `JWT_SECRET` | JWT secret key | Required |
| `JWT_EXPIRES_IN` | JWT expiration time | 24h |
| `ALLOWED_ORIGINS` | CORS allowed origins | http://localhost:3000 |
| `LOG_LEVEL` | Logging level | info |
| `BCRYPT_SALT_ROUNDS` | BCrypt salt rounds | 12 |
## Security Considerations
1. **Environment Variables**: Never commit sensitive data to version control
2. **JWT Secret**: Use a strong, random JWT secret in production
3. **Rate Limiting**: Adjust rate limits based on your requirements
4. **Input Validation**: All inputs are validated and sanitized
5. **Password Security**: Passwords are hashed using BCrypt with salt rounds
6. **Account Lockout**: Accounts are locked after 5 failed login attempts
7. **CORS**: Configure CORS origins for production
8. **Security Headers**: Helmet.js provides security headers
## Performance Optimizations
1. **Database Indexing**: Indexes on frequently queried fields
2. **Pagination**: Efficient pagination for large datasets
3. **Connection Pooling**: MongoDB connection pooling
4. **Compression**: Gzip compression for responses
5. **Caching**: Ready for Redis integration
## Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Add tests for new functionality
5. Ensure all tests pass
6. Submit a pull request
## License
MIT License - see the [LICENSE](LICENSE) file for details.
## Support
For support, please open an issue in the GitHub repository or contact the development team.
## Changelog
### v1.0.0
- Initial release
- User management functionality
- Authentication and authorization
- API endpoints
- Security features
- Testing suite

View File

@@ -0,0 +1,85 @@
{
"name": "user-management",
"version": "1.0.0",
"description": "A comprehensive user management system for testing Code Index MCP",
"main": "src/server.js",
"scripts": {
"start": "node src/server.js",
"dev": "nodemon src/server.js",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint src/ --fix",
"format": "prettier --write src/"
},
"keywords": [
"user-management",
"nodejs",
"express",
"authentication",
"api"
],
"author": "Test Author",
"license": "MIT",
"dependencies": {
"express": "^4.18.2",
"mongoose": "^7.4.1",
"bcryptjs": "^2.4.3",
"jsonwebtoken": "^9.0.1",
"joi": "^17.9.2",
"cors": "^2.8.5",
"helmet": "^7.0.0",
"express-rate-limit": "^6.8.1",
"winston": "^3.10.0",
"dotenv": "^16.3.1",
"uuid": "^9.0.0",
"morgan": "^1.10.0",
"compression": "^1.7.4",
"express-validator": "^7.0.1"
},
"devDependencies": {
"nodemon": "^3.0.1",
"jest": "^29.6.1",
"supertest": "^6.3.3",
"eslint": "^8.45.0",
"prettier": "^3.0.0",
"mongodb-memory-server": "^8.14.0"
},
"engines": {
"node": ">=16.0.0"
},
"jest": {
"testEnvironment": "node",
"coverageDirectory": "coverage",
"collectCoverageFrom": [
"src/**/*.js",
"!src/server.js"
]
},
"eslintConfig": {
"env": {
"node": true,
"es2021": true,
"jest": true
},
"extends": [
"eslint:recommended"
],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"rules": {
"no-console": "warn",
"no-unused-vars": "error",
"prefer-const": "error",
"no-var": "error"
}
},
"prettier": {
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5"
}
}

View File

@@ -0,0 +1,138 @@
const mongoose = require('mongoose');
const logger = require('../utils/logger');
/**
* Database connection configuration
*/
class Database {
constructor() {
this.mongoURI = process.env.MONGODB_URI || 'mongodb://localhost:27017/user-management';
this.options = {
useNewUrlParser: true,
useUnifiedTopology: true,
maxPoolSize: 10,
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000,
family: 4,
};
}
/**
* Connect to MongoDB
*/
async connect() {
try {
await mongoose.connect(this.mongoURI, this.options);
logger.info('MongoDB connected successfully');
// Handle connection events
mongoose.connection.on('error', (err) => {
logger.error('MongoDB connection error:', err);
});
mongoose.connection.on('disconnected', () => {
logger.warn('MongoDB disconnected');
});
mongoose.connection.on('reconnected', () => {
logger.info('MongoDB reconnected');
});
// Handle process termination
process.on('SIGINT', this.gracefulShutdown.bind(this));
process.on('SIGTERM', this.gracefulShutdown.bind(this));
} catch (error) {
logger.error('MongoDB connection failed:', error);
process.exit(1);
}
}
/**
* Disconnect from MongoDB
*/
async disconnect() {
try {
await mongoose.disconnect();
logger.info('MongoDB disconnected successfully');
} catch (error) {
logger.error('MongoDB disconnection error:', error);
}
}
/**
* Graceful shutdown
*/
async gracefulShutdown(signal) {
logger.info(`Received ${signal}. Graceful shutdown...`);
try {
await this.disconnect();
process.exit(0);
} catch (error) {
logger.error('Error during graceful shutdown:', error);
process.exit(1);
}
}
/**
* Get connection status
*/
getConnectionStatus() {
const states = {
0: 'disconnected',
1: 'connected',
2: 'connecting',
3: 'disconnecting',
};
return states[mongoose.connection.readyState] || 'unknown';
}
/**
* Check if database is connected
*/
isConnected() {
return mongoose.connection.readyState === 1;
}
/**
* Drop database (for testing)
*/
async dropDatabase() {
if (process.env.NODE_ENV === 'test') {
try {
await mongoose.connection.db.dropDatabase();
logger.info('Test database dropped');
} catch (error) {
logger.error('Error dropping test database:', error);
}
} else {
logger.warn('Database drop attempted in non-test environment');
}
}
/**
* Get database statistics
*/
async getStats() {
try {
const stats = await mongoose.connection.db.stats();
return {
database: mongoose.connection.name,
collections: stats.collections,
dataSize: stats.dataSize,
storageSize: stats.storageSize,
indexes: stats.indexes,
indexSize: stats.indexSize,
objects: stats.objects,
};
} catch (error) {
logger.error('Error getting database stats:', error);
return null;
}
}
}
// Create singleton instance
const database = new Database();
module.exports = database;

View File

@@ -0,0 +1,165 @@
const jwt = require('jsonwebtoken');
const { User } = require('../models/User');
const { AuthenticationError, AuthorizationError } = require('../utils/errors');
const logger = require('../utils/logger');
/**
* Authentication middleware
* Verifies JWT token and attaches user to request object
*/
const auth = async (req, res, next) => {
try {
// Get token from header
const authHeader = req.header('Authorization');
const token = authHeader && authHeader.startsWith('Bearer ')
? authHeader.substring(7)
: null;
if (!token) {
throw new AuthenticationError('Access denied. No token provided.');
}
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'fallback-secret');
// Get user from database
const user = await User.findOne({ id: decoded.id });
if (!user) {
throw new AuthenticationError('Invalid token. User not found.');
}
// Check if user is active
if (!user.isActive) {
throw new AuthenticationError('User account is not active.');
}
// Attach user to request object
req.user = user;
next();
} catch (error) {
if (error.name === 'JsonWebTokenError') {
logger.warn('Invalid JWT token attempted');
next(new AuthenticationError('Invalid token'));
} else if (error.name === 'TokenExpiredError') {
logger.warn('Expired JWT token attempted');
next(new AuthenticationError('Token expired'));
} else {
logger.error('Authentication error:', error);
next(error);
}
}
};
/**
* Authorization middleware factory
* Creates middleware that checks if user has required role
*/
const authorize = (roles) => {
return (req, res, next) => {
if (!req.user) {
return next(new AuthenticationError('Authentication required'));
}
// Convert single role to array
const allowedRoles = Array.isArray(roles) ? roles : [roles];
// Check if user has required role
if (!allowedRoles.includes(req.user.role)) {
logger.warn(`User ${req.user.username} attempted to access resource requiring roles: ${allowedRoles.join(', ')}`);
return next(new AuthorizationError('Insufficient permissions'));
}
next();
};
};
/**
* Permission-based authorization middleware
* Checks if user has specific permission
*/
const requirePermission = (permission) => {
return (req, res, next) => {
if (!req.user) {
return next(new AuthenticationError('Authentication required'));
}
if (!req.user.hasPermission(permission)) {
logger.warn(`User ${req.user.username} attempted to access resource requiring permission: ${permission}`);
return next(new AuthorizationError('Insufficient permissions'));
}
next();
};
};
/**
* Self or admin middleware
* Allows access if user is accessing their own data or is an admin
*/
const selfOrAdmin = (req, res, next) => {
if (!req.user) {
return next(new AuthenticationError('Authentication required'));
}
const targetUserId = req.params.id;
const isAdmin = req.user.role === 'admin';
const isSelf = req.user.id === targetUserId;
if (!isAdmin && !isSelf) {
logger.warn(`User ${req.user.username} attempted to access another user's data`);
return next(new AuthorizationError('Access denied'));
}
next();
};
/**
* Admin only middleware
* Allows access only for admin users
*/
const adminOnly = authorize(['admin']);
/**
* User or admin middleware
* Allows access for user role and above
*/
const userOrAdmin = authorize(['user', 'admin']);
/**
* Optional authentication middleware
* Authenticates user if token is provided, but doesn't require it
*/
const optionalAuth = async (req, res, next) => {
try {
const authHeader = req.header('Authorization');
const token = authHeader && authHeader.startsWith('Bearer ')
? authHeader.substring(7)
: null;
if (!token) {
return next();
}
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'fallback-secret');
const user = await User.findOne({ id: decoded.id });
if (user && user.isActive) {
req.user = user;
}
next();
} catch (error) {
// Don't fail on optional auth, just continue without user
next();
}
};
module.exports = {
auth,
authorize,
requirePermission,
selfOrAdmin,
adminOnly,
userOrAdmin,
optionalAuth,
};

View File

@@ -0,0 +1,122 @@
const rateLimit = require('express-rate-limit');
const { RateLimitError } = require('../utils/errors');
const logger = require('../utils/logger');
/**
* General rate limiter
* Limits requests per IP address
*/
const generalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: {
error: {
message: 'Too many requests from this IP, please try again later.',
statusCode: 429,
},
},
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
handler: (req, res) => {
logger.warn(`Rate limit exceeded for IP: ${req.ip}`);
const error = new RateLimitError('Too many requests from this IP, please try again later.');
res.status(429).json({
success: false,
error: {
message: error.message,
statusCode: error.statusCode,
},
});
},
});
/**
* Authentication rate limiter
* Stricter limits for login attempts
*/
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // limit each IP to 5 login attempts per windowMs
message: {
error: {
message: 'Too many login attempts from this IP, please try again later.',
statusCode: 429,
},
},
standardHeaders: true,
legacyHeaders: false,
handler: (req, res) => {
logger.warn(`Authentication rate limit exceeded for IP: ${req.ip}`);
const error = new RateLimitError('Too many login attempts from this IP, please try again later.');
res.status(429).json({
success: false,
error: {
message: error.message,
statusCode: error.statusCode,
},
});
},
});
/**
* User creation rate limiter
* Moderate limits for user registration
*/
const createUserLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 10, // limit each IP to 10 user creations per hour
message: {
error: {
message: 'Too many accounts created from this IP, please try again later.',
statusCode: 429,
},
},
standardHeaders: true,
legacyHeaders: false,
handler: (req, res) => {
logger.warn(`User creation rate limit exceeded for IP: ${req.ip}`);
const error = new RateLimitError('Too many accounts created from this IP, please try again later.');
res.status(429).json({
success: false,
error: {
message: error.message,
statusCode: error.statusCode,
},
});
},
});
/**
* Password reset rate limiter
* Limits password reset attempts
*/
const passwordResetLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 3, // limit each IP to 3 password reset attempts per hour
message: {
error: {
message: 'Too many password reset attempts from this IP, please try again later.',
statusCode: 429,
},
},
standardHeaders: true,
legacyHeaders: false,
handler: (req, res) => {
logger.warn(`Password reset rate limit exceeded for IP: ${req.ip}`);
const error = new RateLimitError('Too many password reset attempts from this IP, please try again later.');
res.status(429).json({
success: false,
error: {
message: error.message,
statusCode: error.statusCode,
},
});
},
});
module.exports = {
generalLimiter,
authLimiter,
createUserLimiter,
passwordResetLimiter,
};

View File

@@ -0,0 +1,29 @@
const { validationResult } = require('express-validator');
const { ValidationError } = require('../utils/errors');
/**
* Validation middleware
* Checks for validation errors and returns appropriate error response
*/
const validate = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
const errorMessages = errors.array().map(error => ({
field: error.path,
message: error.msg,
value: error.value,
}));
const validationError = new ValidationError(
'Validation failed',
errorMessages
);
return next(validationError);
}
next();
};
module.exports = validate;

View File

@@ -0,0 +1,333 @@
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const { v4: uuidv4 } = require('uuid');
// User roles enumeration
const USER_ROLES = {
ADMIN: 'admin',
USER: 'user',
GUEST: 'guest',
};
// User status enumeration
const USER_STATUS = {
ACTIVE: 'active',
INACTIVE: 'inactive',
SUSPENDED: 'suspended',
DELETED: 'deleted',
};
// User schema definition
const userSchema = new mongoose.Schema(
{
id: {
type: String,
default: uuidv4,
unique: true,
required: true,
},
username: {
type: String,
required: [true, 'Username is required'],
unique: true,
minlength: [3, 'Username must be at least 3 characters'],
maxlength: [20, 'Username cannot exceed 20 characters'],
match: [/^[a-zA-Z0-9_]+$/, 'Username can only contain letters, numbers, and underscores'],
},
email: {
type: String,
unique: true,
sparse: true,
match: [/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/, 'Please enter a valid email'],
},
name: {
type: String,
required: [true, 'Name is required'],
minlength: [1, 'Name is required'],
maxlength: [100, 'Name cannot exceed 100 characters'],
},
age: {
type: Number,
min: [0, 'Age cannot be negative'],
max: [150, 'Age cannot exceed 150'],
},
password: {
type: String,
required: [true, 'Password is required'],
minlength: [8, 'Password must be at least 8 characters'],
select: false, // Don't include in queries by default
},
role: {
type: String,
enum: Object.values(USER_ROLES),
default: USER_ROLES.USER,
},
status: {
type: String,
enum: Object.values(USER_STATUS),
default: USER_STATUS.ACTIVE,
},
lastLogin: {
type: Date,
default: null,
},
loginAttempts: {
type: Number,
default: 0,
},
permissions: {
type: [String],
default: [],
},
metadata: {
type: mongoose.Schema.Types.Mixed,
default: {},
},
},
{
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true },
}
);
// Virtual for user response (without sensitive data)
userSchema.virtual('response').get(function() {
return {
id: this.id,
username: this.username,
email: this.email,
name: this.name,
age: this.age,
role: this.role,
status: this.status,
lastLogin: this.lastLogin,
permissions: this.permissions,
metadata: this.metadata,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
};
});
// Virtual for checking if user is active
userSchema.virtual('isActive').get(function() {
return this.status === USER_STATUS.ACTIVE;
});
// Virtual for checking if user is admin
userSchema.virtual('isAdmin').get(function() {
return this.role === USER_ROLES.ADMIN;
});
// Virtual for checking if user is locked
userSchema.virtual('isLocked').get(function() {
return this.loginAttempts >= 5 || this.status === USER_STATUS.SUSPENDED;
});
// Pre-save middleware to hash password
userSchema.pre('save', async function(next) {
// Only hash the password if it has been modified (or is new)
if (!this.isModified('password')) return next();
try {
// Hash password with cost of 12
const hashedPassword = await bcrypt.hash(this.password, 12);
this.password = hashedPassword;
next();
} catch (error) {
next(error);
}
});
// Instance method to check password
userSchema.methods.checkPassword = async function(candidatePassword) {
return await bcrypt.compare(candidatePassword, this.password);
};
// Instance method to generate JWT token
userSchema.methods.generateToken = function() {
return jwt.sign(
{
id: this.id,
username: this.username,
role: this.role
},
process.env.JWT_SECRET || 'fallback-secret',
{
expiresIn: process.env.JWT_EXPIRES_IN || '24h',
issuer: 'user-management-system'
}
);
};
// Instance method to add permission
userSchema.methods.addPermission = function(permission) {
if (!this.permissions.includes(permission)) {
this.permissions.push(permission);
}
};
// Instance method to remove permission
userSchema.methods.removePermission = function(permission) {
this.permissions = this.permissions.filter(p => p !== permission);
};
// Instance method to check permission
userSchema.methods.hasPermission = function(permission) {
return this.permissions.includes(permission);
};
// Instance method to record successful login
userSchema.methods.recordLogin = function() {
this.lastLogin = new Date();
this.loginAttempts = 0;
};
// Instance method to record failed login attempt
userSchema.methods.recordFailedLogin = function() {
this.loginAttempts += 1;
if (this.loginAttempts >= 5) {
this.status = USER_STATUS.SUSPENDED;
}
};
// Instance method to reset login attempts
userSchema.methods.resetLoginAttempts = function() {
this.loginAttempts = 0;
};
// Instance method to activate user
userSchema.methods.activate = function() {
this.status = USER_STATUS.ACTIVE;
this.loginAttempts = 0;
};
// Instance method to deactivate user
userSchema.methods.deactivate = function() {
this.status = USER_STATUS.INACTIVE;
};
// Instance method to suspend user
userSchema.methods.suspend = function() {
this.status = USER_STATUS.SUSPENDED;
};
// Instance method to delete user (soft delete)
userSchema.methods.delete = function() {
this.status = USER_STATUS.DELETED;
};
// Instance method to get metadata
userSchema.methods.getMetadata = function(key, defaultValue = null) {
return this.metadata[key] || defaultValue;
};
// Instance method to set metadata
userSchema.methods.setMetadata = function(key, value) {
this.metadata[key] = value;
};
// Instance method to remove metadata
userSchema.methods.removeMetadata = function(key) {
delete this.metadata[key];
};
// Instance method to validate user data
userSchema.methods.validateUser = function() {
const errors = [];
if (!this.username || this.username.length < 3) {
errors.push('Username must be at least 3 characters');
}
if (!this.name || this.name.length === 0) {
errors.push('Name is required');
}
if (this.age && (this.age < 0 || this.age > 150)) {
errors.push('Age must be between 0 and 150');
}
if (this.email && !this.email.match(/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/)) {
errors.push('Email format is invalid');
}
return errors;
};
// Static method to find by username
userSchema.statics.findByUsername = function(username) {
return this.findOne({ username });
};
// Static method to find by email
userSchema.statics.findByEmail = function(email) {
return this.findOne({ email });
};
// Static method to find active users
userSchema.statics.findActive = function() {
return this.find({ status: USER_STATUS.ACTIVE });
};
// Static method to find by role
userSchema.statics.findByRole = function(role) {
return this.find({ role });
};
// Static method to search users
userSchema.statics.searchUsers = function(query, options = {}) {
const searchRegex = new RegExp(query, 'i');
const searchQuery = {
$or: [
{ username: searchRegex },
{ name: searchRegex },
{ email: searchRegex },
],
};
return this.find(searchQuery, null, options);
};
// Static method to get user statistics
userSchema.statics.getUserStats = async function() {
const stats = await this.aggregate([
{
$group: {
_id: null,
total: { $sum: 1 },
active: { $sum: { $cond: [{ $eq: ['$status', USER_STATUS.ACTIVE] }, 1, 0] } },
admin: { $sum: { $cond: [{ $eq: ['$role', USER_ROLES.ADMIN] }, 1, 0] } },
user: { $sum: { $cond: [{ $eq: ['$role', USER_ROLES.USER] }, 1, 0] } },
guest: { $sum: { $cond: [{ $eq: ['$role', USER_ROLES.GUEST] }, 1, 0] } },
withEmail: { $sum: { $cond: [{ $ne: ['$email', null] }, 1, 0] } },
},
},
]);
return stats[0] || {
total: 0,
active: 0,
admin: 0,
user: 0,
guest: 0,
withEmail: 0,
};
};
// Index for performance
userSchema.index({ username: 1 });
userSchema.index({ email: 1 });
userSchema.index({ role: 1 });
userSchema.index({ status: 1 });
userSchema.index({ createdAt: -1 });
// Export model and constants
const User = mongoose.model('User', userSchema);
module.exports = {
User,
USER_ROLES,
USER_STATUS,
};

View File

@@ -0,0 +1,268 @@
const express = require('express');
const { body, param, query } = require('express-validator');
const UserService = require('../services/UserService');
const { asyncHandler, createSuccessResponse, createErrorResponse } = require('../utils/errors');
const auth = require('../middleware/auth');
const validate = require('../middleware/validate');
const logger = require('../utils/logger');
const router = express.Router();
const userService = new UserService();
// User creation validation
const createUserValidation = [
body('username')
.isLength({ min: 3, max: 20 })
.withMessage('Username must be between 3 and 20 characters')
.matches(/^[a-zA-Z0-9_]+$/)
.withMessage('Username can only contain letters, numbers, and underscores'),
body('name')
.isLength({ min: 1, max: 100 })
.withMessage('Name must be between 1 and 100 characters'),
body('email')
.optional()
.isEmail()
.withMessage('Please provide a valid email address'),
body('age')
.optional()
.isInt({ min: 0, max: 150 })
.withMessage('Age must be between 0 and 150'),
body('password')
.isLength({ min: 8 })
.withMessage('Password must be at least 8 characters long'),
body('role')
.optional()
.isIn(['admin', 'user', 'guest'])
.withMessage('Role must be admin, user, or guest'),
];
// User update validation
const updateUserValidation = [
param('id').notEmpty().withMessage('User ID is required'),
body('username')
.optional()
.isLength({ min: 3, max: 20 })
.withMessage('Username must be between 3 and 20 characters')
.matches(/^[a-zA-Z0-9_]+$/)
.withMessage('Username can only contain letters, numbers, and underscores'),
body('name')
.optional()
.isLength({ min: 1, max: 100 })
.withMessage('Name must be between 1 and 100 characters'),
body('email')
.optional()
.isEmail()
.withMessage('Please provide a valid email address'),
body('age')
.optional()
.isInt({ min: 0, max: 150 })
.withMessage('Age must be between 0 and 150'),
body('role')
.optional()
.isIn(['admin', 'user', 'guest'])
.withMessage('Role must be admin, user, or guest'),
];
// Password change validation
const passwordChangeValidation = [
param('id').notEmpty().withMessage('User ID is required'),
body('currentPassword')
.isLength({ min: 1 })
.withMessage('Current password is required'),
body('newPassword')
.isLength({ min: 8 })
.withMessage('New password must be at least 8 characters long'),
];
// Authentication validation
const authValidation = [
body('username')
.isLength({ min: 3 })
.withMessage('Username must be at least 3 characters'),
body('password')
.isLength({ min: 1 })
.withMessage('Password is required'),
];
// Search validation
const searchValidation = [
query('q')
.isLength({ min: 1 })
.withMessage('Search query is required'),
query('page')
.optional()
.isInt({ min: 1 })
.withMessage('Page must be a positive integer'),
query('limit')
.optional()
.isInt({ min: 1, max: 100 })
.withMessage('Limit must be between 1 and 100'),
];
// @route POST /api/users
// @desc Create a new user
// @access Public
router.post('/', createUserValidation, validate, asyncHandler(async (req, res) => {
const user = await userService.createUser(req.body);
logger.info(`User created via API: ${user.username}`);
res.status(201).json(createSuccessResponse(user, 'User created successfully'));
}));
// @route POST /api/users/auth
// @desc Authenticate user
// @access Public
router.post('/auth', authValidation, validate, asyncHandler(async (req, res) => {
const { username, password } = req.body;
const result = await userService.authenticateUser(username, password);
logger.info(`User authenticated via API: ${username}`);
res.json(createSuccessResponse(result, 'Authentication successful'));
}));
// @route GET /api/users
// @desc Get all users with pagination
// @access Private (Admin only)
router.get('/', auth, asyncHandler(async (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
const filter = {};
// Add filtering by role if provided
if (req.query.role) {
filter.role = req.query.role;
}
// Add filtering by status if provided
if (req.query.status) {
filter.status = req.query.status;
}
const result = await userService.getAllUsers(page, limit, filter);
res.json(createSuccessResponse(result, 'Users retrieved successfully'));
}));
// @route GET /api/users/search
// @desc Search users
// @access Private
router.get('/search', auth, searchValidation, validate, asyncHandler(async (req, res) => {
const { q: query, page = 1, limit = 20 } = req.query;
const result = await userService.searchUsers(query, parseInt(page), parseInt(limit));
res.json(createSuccessResponse(result, 'Search completed successfully'));
}));
// @route GET /api/users/stats
// @desc Get user statistics
// @access Private (Admin only)
router.get('/stats', auth, asyncHandler(async (req, res) => {
const stats = await userService.getUserStats();
res.json(createSuccessResponse(stats, 'Statistics retrieved successfully'));
}));
// @route GET /api/users/export
// @desc Export all users
// @access Private (Admin only)
router.get('/export', auth, asyncHandler(async (req, res) => {
const users = await userService.exportUsers();
res.json(createSuccessResponse(users, 'Users exported successfully'));
}));
// @route GET /api/users/active
// @desc Get active users
// @access Private
router.get('/active', auth, asyncHandler(async (req, res) => {
const users = await userService.getActiveUsers();
res.json(createSuccessResponse(users, 'Active users retrieved successfully'));
}));
// @route GET /api/users/role/:role
// @desc Get users by role
// @access Private (Admin only)
router.get('/role/:role', auth, asyncHandler(async (req, res) => {
const { role } = req.params;
const users = await userService.getUsersByRole(role);
res.json(createSuccessResponse(users, `Users with role ${role} retrieved successfully`));
}));
// @route GET /api/users/:id
// @desc Get user by ID
// @access Private
router.get('/:id', auth, asyncHandler(async (req, res) => {
const user = await userService.getUserById(req.params.id);
res.json(createSuccessResponse(user, 'User retrieved successfully'));
}));
// @route GET /api/users/:id/activity
// @desc Get user activity
// @access Private (Admin or same user)
router.get('/:id/activity', auth, asyncHandler(async (req, res) => {
const activity = await userService.getUserActivity(req.params.id);
res.json(createSuccessResponse(activity, 'User activity retrieved successfully'));
}));
// @route PUT /api/users/:id
// @desc Update user
// @access Private (Admin or same user)
router.put('/:id', auth, updateUserValidation, validate, asyncHandler(async (req, res) => {
const user = await userService.updateUser(req.params.id, req.body);
logger.info(`User updated via API: ${user.username}`);
res.json(createSuccessResponse(user, 'User updated successfully'));
}));
// @route PUT /api/users/:id/password
// @desc Change user password
// @access Private (Admin or same user)
router.put('/:id/password', auth, passwordChangeValidation, validate, asyncHandler(async (req, res) => {
const { currentPassword, newPassword } = req.body;
await userService.changePassword(req.params.id, currentPassword, newPassword);
logger.info(`Password changed via API for user: ${req.params.id}`);
res.json(createSuccessResponse(null, 'Password changed successfully'));
}));
// @route PUT /api/users/:id/reset-password
// @desc Reset user password (Admin only)
// @access Private (Admin only)
router.put('/:id/reset-password', auth, asyncHandler(async (req, res) => {
const { newPassword } = req.body;
await userService.resetPassword(req.params.id, newPassword);
logger.info(`Password reset via API for user: ${req.params.id}`);
res.json(createSuccessResponse(null, 'Password reset successfully'));
}));
// @route PUT /api/users/:id/permissions
// @desc Add permission to user
// @access Private (Admin only)
router.put('/:id/permissions', auth, asyncHandler(async (req, res) => {
const { permission } = req.body;
await userService.addPermission(req.params.id, permission);
logger.info(`Permission added via API for user: ${req.params.id}`);
res.json(createSuccessResponse(null, 'Permission added successfully'));
}));
// @route DELETE /api/users/:id/permissions
// @desc Remove permission from user
// @access Private (Admin only)
router.delete('/:id/permissions', auth, asyncHandler(async (req, res) => {
const { permission } = req.body;
await userService.removePermission(req.params.id, permission);
logger.info(`Permission removed via API for user: ${req.params.id}`);
res.json(createSuccessResponse(null, 'Permission removed successfully'));
}));
// @route DELETE /api/users/:id
// @desc Delete user (soft delete)
// @access Private (Admin only)
router.delete('/:id', auth, asyncHandler(async (req, res) => {
await userService.deleteUser(req.params.id);
logger.info(`User deleted via API: ${req.params.id}`);
res.json(createSuccessResponse(null, 'User deleted successfully'));
}));
// @route DELETE /api/users/:id/hard
// @desc Hard delete user (permanent)
// @access Private (Admin only)
router.delete('/:id/hard', auth, asyncHandler(async (req, res) => {
await userService.hardDeleteUser(req.params.id);
logger.info(`User permanently deleted via API: ${req.params.id}`);
res.json(createSuccessResponse(null, 'User permanently deleted'));
}));
module.exports = router;

View File

@@ -0,0 +1,190 @@
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const compression = require('compression');
const morgan = require('morgan');
const database = require('./config/database');
const userRoutes = require('./routes/userRoutes');
const { generalLimiter } = require('./middleware/rateLimiter');
const { globalErrorHandler, handleNotFound } = require('./utils/errors');
const logger = require('./utils/logger');
/**
* Express application setup
*/
class App {
constructor() {
this.app = express();
this.port = process.env.PORT || 3000;
this.setupMiddleware();
this.setupRoutes();
this.setupErrorHandling();
}
/**
* Setup middleware
*/
setupMiddleware() {
// Security middleware
this.app.use(helmet());
// CORS configuration
this.app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
credentials: true,
}));
// Compression middleware
this.app.use(compression());
// Body parsing middleware
this.app.use(express.json({ limit: '10mb' }));
this.app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// Logging middleware
this.app.use(morgan('combined', { stream: logger.stream }));
// Rate limiting
this.app.use(generalLimiter);
// Request ID middleware
this.app.use((req, res, next) => {
req.requestId = Math.random().toString(36).substr(2, 9);
res.set('X-Request-ID', req.requestId);
next();
});
// Health check endpoint
this.app.get('/health', (req, res) => {
res.json({
status: 'OK',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
database: database.getConnectionStatus(),
version: process.env.npm_package_version || '1.0.0',
});
});
}
/**
* Setup routes
*/
setupRoutes() {
// API routes
this.app.use('/api/users', userRoutes);
// Root endpoint
this.app.get('/', (req, res) => {
res.json({
message: 'User Management API',
version: '1.0.0',
endpoints: {
health: '/health',
users: '/api/users',
auth: '/api/users/auth',
},
});
});
}
/**
* Setup error handling
*/
setupErrorHandling() {
// Handle 404 for unknown routes
this.app.use(handleNotFound);
// Global error handler
this.app.use(globalErrorHandler);
}
/**
* Start the server
*/
async start() {
try {
// Connect to database
await database.connect();
// Start server
this.server = this.app.listen(this.port, () => {
logger.info(`Server running on port ${this.port}`);
logger.info(`Environment: ${process.env.NODE_ENV || 'development'}`);
logger.info(`Health check: http://localhost:${this.port}/health`);
});
// Handle server errors
this.server.on('error', (error) => {
if (error.syscall !== 'listen') {
throw error;
}
const bind = typeof this.port === 'string' ? `Pipe ${this.port}` : `Port ${this.port}`;
switch (error.code) {
case 'EACCES':
logger.error(`${bind} requires elevated privileges`);
process.exit(1);
break;
case 'EADDRINUSE':
logger.error(`${bind} is already in use`);
process.exit(1);
break;
default:
throw error;
}
});
// Graceful shutdown
process.on('SIGTERM', this.gracefulShutdown.bind(this));
process.on('SIGINT', this.gracefulShutdown.bind(this));
} catch (error) {
logger.error('Failed to start server:', error);
process.exit(1);
}
}
/**
* Graceful shutdown
*/
async gracefulShutdown(signal) {
logger.info(`Received ${signal}. Graceful shutdown...`);
if (this.server) {
this.server.close(async () => {
logger.info('HTTP server closed');
try {
await database.disconnect();
logger.info('Database disconnected');
process.exit(0);
} catch (error) {
logger.error('Error during graceful shutdown:', error);
process.exit(1);
}
});
}
}
/**
* Get Express app instance
*/
getApp() {
return this.app;
}
}
// Create and start the application
const app = new App();
// Start server if not in test environment
if (process.env.NODE_ENV !== 'test') {
app.start();
}
// Export for testing
module.exports = app.getApp();

View File

@@ -0,0 +1,493 @@
const { User, USER_ROLES, USER_STATUS } = require('../models/User');
const { AppError } = require('../utils/errors');
const logger = require('../utils/logger');
/**
* UserService class handles all user-related business logic
*/
class UserService {
/**
* Create a new user
* @param {Object} userData - User data object
* @returns {Promise<Object>} Created user response
*/
async createUser(userData) {
try {
// Check if username already exists
const existingUsername = await User.findByUsername(userData.username);
if (existingUsername) {
throw new AppError('Username already exists', 400);
}
// Check if email already exists (if provided)
if (userData.email) {
const existingEmail = await User.findByEmail(userData.email);
if (existingEmail) {
throw new AppError('Email already exists', 400);
}
}
// Create new user
const user = new User(userData);
// Validate user data
const validationErrors = user.validateUser();
if (validationErrors.length > 0) {
throw new AppError(validationErrors.join(', '), 400);
}
await user.save();
logger.info(`User created successfully: ${user.username}`);
return user.response;
} catch (error) {
logger.error('Error creating user:', error);
throw error;
}
}
/**
* Get user by ID
* @param {string} id - User ID
* @returns {Promise<Object>} User response
*/
async getUserById(id) {
try {
const user = await User.findOne({ id });
if (!user) {
throw new AppError('User not found', 404);
}
return user.response;
} catch (error) {
logger.error('Error getting user by ID:', error);
throw error;
}
}
/**
* Get user by username
* @param {string} username - Username
* @returns {Promise<Object>} User response
*/
async getUserByUsername(username) {
try {
const user = await User.findByUsername(username);
if (!user) {
throw new AppError('User not found', 404);
}
return user.response;
} catch (error) {
logger.error('Error getting user by username:', error);
throw error;
}
}
/**
* Get user by email
* @param {string} email - Email address
* @returns {Promise<Object>} User response
*/
async getUserByEmail(email) {
try {
const user = await User.findByEmail(email);
if (!user) {
throw new AppError('User not found', 404);
}
return user.response;
} catch (error) {
logger.error('Error getting user by email:', error);
throw error;
}
}
/**
* Update user
* @param {string} id - User ID
* @param {Object} updateData - Update data
* @returns {Promise<Object>} Updated user response
*/
async updateUser(id, updateData) {
try {
const user = await User.findOne({ id });
if (!user) {
throw new AppError('User not found', 404);
}
// Apply updates
Object.keys(updateData).forEach(key => {
if (key !== 'password' && key !== 'id') {
user[key] = updateData[key];
}
});
// Validate updated data
const validationErrors = user.validateUser();
if (validationErrors.length > 0) {
throw new AppError(validationErrors.join(', '), 400);
}
await user.save();
logger.info(`User updated successfully: ${user.username}`);
return user.response;
} catch (error) {
logger.error('Error updating user:', error);
throw error;
}
}
/**
* Delete user (soft delete)
* @param {string} id - User ID
* @returns {Promise<boolean>} Success status
*/
async deleteUser(id) {
try {
const user = await User.findOne({ id });
if (!user) {
throw new AppError('User not found', 404);
}
user.delete();
await user.save();
logger.info(`User deleted successfully: ${user.username}`);
return true;
} catch (error) {
logger.error('Error deleting user:', error);
throw error;
}
}
/**
* Hard delete user (permanent deletion)
* @param {string} id - User ID
* @returns {Promise<boolean>} Success status
*/
async hardDeleteUser(id) {
try {
const result = await User.deleteOne({ id });
if (result.deletedCount === 0) {
throw new AppError('User not found', 404);
}
logger.info(`User permanently deleted: ${id}`);
return true;
} catch (error) {
logger.error('Error hard deleting user:', error);
throw error;
}
}
/**
* Get all users with pagination
* @param {number} page - Page number
* @param {number} limit - Items per page
* @param {Object} filter - Filter criteria
* @returns {Promise<Object>} Paginated users response
*/
async getAllUsers(page = 1, limit = 20, filter = {}) {
try {
const skip = (page - 1) * limit;
const users = await User.find(filter)
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit);
const total = await User.countDocuments(filter);
const totalPages = Math.ceil(total / limit);
return {
users: users.map(user => user.response),
pagination: {
page,
limit,
total,
totalPages,
hasNext: page < totalPages,
hasPrev: page > 1,
},
};
} catch (error) {
logger.error('Error getting all users:', error);
throw error;
}
}
/**
* Get active users
* @returns {Promise<Array>} Active users
*/
async getActiveUsers() {
try {
const users = await User.findActive();
return users.map(user => user.response);
} catch (error) {
logger.error('Error getting active users:', error);
throw error;
}
}
/**
* Get users by role
* @param {string} role - User role
* @returns {Promise<Array>} Users with specified role
*/
async getUsersByRole(role) {
try {
const users = await User.findByRole(role);
return users.map(user => user.response);
} catch (error) {
logger.error('Error getting users by role:', error);
throw error;
}
}
/**
* Search users
* @param {string} query - Search query
* @param {number} page - Page number
* @param {number} limit - Items per page
* @returns {Promise<Object>} Search results
*/
async searchUsers(query, page = 1, limit = 20) {
try {
const skip = (page - 1) * limit;
const users = await User.searchUsers(query, {
skip,
limit,
sort: { createdAt: -1 },
});
// Count total matching users
const totalUsers = await User.searchUsers(query);
const total = totalUsers.length;
const totalPages = Math.ceil(total / limit);
return {
users: users.map(user => user.response),
query,
pagination: {
page,
limit,
total,
totalPages,
hasNext: page < totalPages,
hasPrev: page > 1,
},
};
} catch (error) {
logger.error('Error searching users:', error);
throw error;
}
}
/**
* Get user statistics
* @returns {Promise<Object>} User statistics
*/
async getUserStats() {
try {
const stats = await User.getUserStats();
return stats;
} catch (error) {
logger.error('Error getting user statistics:', error);
throw error;
}
}
/**
* Authenticate user
* @param {string} username - Username
* @param {string} password - Password
* @returns {Promise<Object>} Authentication result
*/
async authenticateUser(username, password) {
try {
const user = await User.findByUsername(username).select('+password');
if (!user || !(await user.checkPassword(password))) {
// Record failed login attempt if user exists
if (user) {
user.recordFailedLogin();
await user.save();
}
throw new AppError('Invalid username or password', 401);
}
if (!user.isActive) {
throw new AppError('User account is not active', 401);
}
if (user.isLocked) {
throw new AppError('User account is locked', 401);
}
// Record successful login
user.recordLogin();
await user.save();
// Generate token
const token = user.generateToken();
logger.info(`User authenticated successfully: ${user.username}`);
return {
user: user.response,
token,
};
} catch (error) {
logger.error('Error authenticating user:', error);
throw error;
}
}
/**
* Change user password
* @param {string} id - User ID
* @param {string} currentPassword - Current password
* @param {string} newPassword - New password
* @returns {Promise<boolean>} Success status
*/
async changePassword(id, currentPassword, newPassword) {
try {
const user = await User.findOne({ id }).select('+password');
if (!user) {
throw new AppError('User not found', 404);
}
if (!(await user.checkPassword(currentPassword))) {
throw new AppError('Current password is incorrect', 400);
}
user.password = newPassword;
await user.save();
logger.info(`Password changed successfully for user: ${user.username}`);
return true;
} catch (error) {
logger.error('Error changing password:', error);
throw error;
}
}
/**
* Reset user password (admin function)
* @param {string} id - User ID
* @param {string} newPassword - New password
* @returns {Promise<boolean>} Success status
*/
async resetPassword(id, newPassword) {
try {
const user = await User.findOne({ id });
if (!user) {
throw new AppError('User not found', 404);
}
user.password = newPassword;
user.resetLoginAttempts();
await user.save();
logger.info(`Password reset successfully for user: ${user.username}`);
return true;
} catch (error) {
logger.error('Error resetting password:', error);
throw error;
}
}
/**
* Add permission to user
* @param {string} id - User ID
* @param {string} permission - Permission to add
* @returns {Promise<boolean>} Success status
*/
async addPermission(id, permission) {
try {
const user = await User.findOne({ id });
if (!user) {
throw new AppError('User not found', 404);
}
user.addPermission(permission);
await user.save();
logger.info(`Permission added to user ${user.username}: ${permission}`);
return true;
} catch (error) {
logger.error('Error adding permission:', error);
throw error;
}
}
/**
* Remove permission from user
* @param {string} id - User ID
* @param {string} permission - Permission to remove
* @returns {Promise<boolean>} Success status
*/
async removePermission(id, permission) {
try {
const user = await User.findOne({ id });
if (!user) {
throw new AppError('User not found', 404);
}
user.removePermission(permission);
await user.save();
logger.info(`Permission removed from user ${user.username}: ${permission}`);
return true;
} catch (error) {
logger.error('Error removing permission:', error);
throw error;
}
}
/**
* Export users data
* @returns {Promise<Array>} Users data for export
*/
async exportUsers() {
try {
const users = await User.find().sort({ createdAt: -1 });
return users.map(user => user.response);
} catch (error) {
logger.error('Error exporting users:', error);
throw error;
}
}
/**
* Get user activity
* @param {string} id - User ID
* @returns {Promise<Object>} User activity data
*/
async getUserActivity(id) {
try {
const user = await User.findOne({ id });
if (!user) {
throw new AppError('User not found', 404);
}
return {
id: user.id,
username: user.username,
lastLogin: user.lastLogin,
loginAttempts: user.loginAttempts,
isActive: user.isActive,
isLocked: user.isLocked,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
};
} catch (error) {
logger.error('Error getting user activity:', error);
throw error;
}
}
}
module.exports = UserService;
// AUTO_REINDEX_MARKER: ci_auto_reindex_test_token_js

View File

@@ -0,0 +1,206 @@
/**
* Custom error classes for the application
*/
/**
* Base application error class
*/
class AppError extends Error {
constructor(message, statusCode = 500, isOperational = true) {
super(message);
this.statusCode = statusCode;
this.isOperational = isOperational;
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
// Capture stack trace
Error.captureStackTrace(this, this.constructor);
}
}
/**
* Validation error class
*/
class ValidationError extends AppError {
constructor(message, errors = []) {
super(message, 400);
this.errors = errors;
}
}
/**
* Authentication error class
*/
class AuthenticationError extends AppError {
constructor(message = 'Authentication failed') {
super(message, 401);
}
}
/**
* Authorization error class
*/
class AuthorizationError extends AppError {
constructor(message = 'Access denied') {
super(message, 403);
}
}
/**
* Not found error class
*/
class NotFoundError extends AppError {
constructor(message = 'Resource not found') {
super(message, 404);
}
}
/**
* Conflict error class
*/
class ConflictError extends AppError {
constructor(message = 'Resource conflict') {
super(message, 409);
}
}
/**
* Rate limit error class
*/
class RateLimitError extends AppError {
constructor(message = 'Too many requests') {
super(message, 429);
}
}
/**
* Database error class
*/
class DatabaseError extends AppError {
constructor(message = 'Database error') {
super(message, 500);
}
}
/**
* External service error class
*/
class ExternalServiceError extends AppError {
constructor(message = 'External service error') {
super(message, 502);
}
}
/**
* Global error handler for Express
*/
const globalErrorHandler = (err, req, res, next) => {
// Default error values
let error = { ...err };
error.message = err.message;
// Log error
console.error('Error:', err);
// Mongoose bad ObjectId
if (err.name === 'CastError') {
const message = 'Resource not found';
error = new NotFoundError(message);
}
// Mongoose duplicate key
if (err.code === 11000) {
const value = err.errmsg.match(/(["'])(\\?.)*?\1/)[0];
const message = `Duplicate field value: ${value}. Please use another value`;
error = new ConflictError(message);
}
// Mongoose validation error
if (err.name === 'ValidationError') {
const errors = Object.values(err.errors).map(val => val.message);
error = new ValidationError('Validation failed', errors);
}
// JWT errors
if (err.name === 'JsonWebTokenError') {
error = new AuthenticationError('Invalid token');
}
if (err.name === 'TokenExpiredError') {
error = new AuthenticationError('Token expired');
}
// Send error response
res.status(error.statusCode || 500).json({
success: false,
error: {
message: error.message,
...(process.env.NODE_ENV === 'development' && { stack: error.stack }),
...(error.errors && { errors: error.errors }),
},
});
};
/**
* Async error handler wrapper
*/
const asyncHandler = (fn) => {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
/**
* Create error response
*/
const createErrorResponse = (message, statusCode = 500, errors = null) => {
const response = {
success: false,
error: {
message,
statusCode,
},
};
if (errors) {
response.error.errors = errors;
}
return response;
};
/**
* Create success response
*/
const createSuccessResponse = (data, message = 'Success') => {
return {
success: true,
message,
data,
};
};
/**
* Handle 404 for unknown routes
*/
const handleNotFound = (req, res, next) => {
const error = new NotFoundError(`Route ${req.originalUrl} not found`);
next(error);
};
module.exports = {
AppError,
ValidationError,
AuthenticationError,
AuthorizationError,
NotFoundError,
ConflictError,
RateLimitError,
DatabaseError,
ExternalServiceError,
globalErrorHandler,
asyncHandler,
createErrorResponse,
createSuccessResponse,
handleNotFound,
};

View File

@@ -0,0 +1,79 @@
const winston = require('winston');
// Define log levels
const levels = {
error: 0,
warn: 1,
info: 2,
http: 3,
debug: 4,
};
// Define colors for each level
const colors = {
error: 'red',
warn: 'yellow',
info: 'green',
http: 'magenta',
debug: 'white',
};
// Tell winston about the colors
winston.addColors(colors);
// Format function
const format = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }),
winston.format.colorize({ all: true }),
winston.format.printf(
(info) => `${info.timestamp} ${info.level}: ${info.message}`
)
);
// Define which transports the logger must use
const transports = [
// Console transport
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
),
}),
// File transport for errors
new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
}),
// File transport for all logs
new winston.transports.File({
filename: 'logs/combined.log',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
}),
];
// Create the logger
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
levels,
format,
transports,
});
// Create a stream object with a 'write' function that will be used by Morgan
logger.stream = {
write: (message) => {
// Remove the trailing newline
logger.http(message.trim());
},
};
module.exports = logger;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
pytest>=7.0.0
pydantic>=1.10.0
click>=8.0.0

View File

@@ -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",
],
)

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
"""Models package for user management system."""
from .person import Person
from .user import User
__all__ = ["Person", "User"]

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
"""Services package for user management system."""
from .user_manager import UserManager
from .auth_service import AuthService
__all__ = ["UserManager", "AuthService"]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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