diff --git a/Readme.md b/Readme.md
index 0d5f935..cb2dc79 100644
--- a/Readme.md
+++ b/Readme.md
@@ -30,6 +30,7 @@ MyDocManager is a real-time document processing application that automatically d
4. **mongodb**: Final database for processing results
5. **frontend**: React interface for monitoring and file access
+
## Data Flow
1. **File Detection**: Watchdog monitors target directory in real-time
@@ -73,8 +74,9 @@ The application is designed for container-based development with hot-reload capa
4. **mongodb**: Final database for processing results
5. **frontend**: React interface for monitoring and file access
-## Project Structure (To be implemented)
+## Project Structure
+```
MyDocManager/
├── docker-compose.yml
├── src/
@@ -85,7 +87,35 @@ MyDocManager/
│ │ │ ├── main.py
│ │ │ ├── file_watcher.py
│ │ │ ├── celery_app.py
-│ │ │ └── api/
+│ │ │ ├── config/
+│ │ │ │ ├── __init__.py
+│ │ │ │ └── settings.py # JWT, MongoDB config
+│ │ │ ├── models/
+│ │ │ │ ├── __init__.py
+│ │ │ │ ├── user.py # User Pydantic models
+│ │ │ │ └── auth.py # Auth Pydantic models
+│ │ │ ├── database/
+│ │ │ │ ├── __init__.py
+│ │ │ │ ├── connection.py # MongoDB connection
+│ │ │ │ └── repositories/
+│ │ │ │ ├── __init__.py
+│ │ │ │ └── user_repository.py # User CRUD operations
+│ │ │ ├── services/
+│ │ │ │ ├── __init__.py
+│ │ │ │ ├── auth_service.py # JWT & password logic
+│ │ │ │ ├── user_service.py # User business logic
+│ │ │ │ └── init_service.py # Admin creation at startup
+│ │ │ ├── api/
+│ │ │ │ ├── __init__.py
+│ │ │ │ ├── dependencies.py # Auth dependencies
+│ │ │ │ └── routes/
+│ │ │ │ ├── __init__.py
+│ │ │ │ ├── auth.py # Authentication routes
+│ │ │ │ └── users.py # User management routes
+│ │ │ └── utils/
+│ │ │ ├── __init__.py
+│ │ │ ├── security.py # Password utilities
+│ │ │ └── exceptions.py # Custom exceptions
│ ├── worker/
│ │ ├── Dockerfile
│ │ ├── requirements.txt
@@ -96,10 +126,43 @@ MyDocManager/
│ └── src/
├── tests/
│ ├── file-processor/
+│ │ ├── test_auth/
+│ │ ├── test_users/
+│ │ └── test_services/
│ └── worker/
├── volumes/
│ └── watched_files/
└── README.md
+```
+
+## Authentication & User Management
+
+### Security Features
+- **JWT Authentication**: Stateless authentication with 24-hour token expiration
+- **Password Security**: bcrypt hashing with automatic salting
+- **Role-Based Access**: Admin and User roles with granular permissions
+- **Protected Routes**: All user management APIs require valid authentication
+- **Auto Admin Creation**: Default admin user created on first startup
+
+### User Roles
+- **Admin**: Full access to user management (create, read, update, delete users)
+- **User**: Limited access (view own profile, access document processing features)
+
+### Authentication Flow
+1. **Login**: User provides credentials → Server validates → Returns JWT token
+2. **API Access**: Client includes JWT in Authorization header
+3. **Token Validation**: Server verifies token signature and expiration
+4. **Role Check**: Server validates user permissions for requested resource
+
+### User Management APIs
+```
+POST /auth/login # Generate JWT token
+GET /users # List all users (admin only)
+POST /users # Create new user (admin only)
+PUT /users/{user_id} # Update user (admin only)
+DELETE /users/{user_id} # Delete user (admin only)
+GET /users/me # Get current user profile (authenticated users)
+```
## Docker Commands Reference
@@ -211,7 +274,14 @@ curl -X POST http://localhost:8000/test-task \
# Monitor Celery tasks
docker-compose logs -f worker
```
+## Default Admin User
+On first startup, the application automatically creates a default admin user:
+- **Username**: `admin`
+- **Password**: `admin`
+- **Role**: `admin`
+- **Email**: `admin@mydocmanager.local`
+**⚠️ Important**: Change the default admin password immediately after first login in production environments.
## Key Implementation Notes
@@ -221,6 +291,12 @@ docker-compose logs -f worker
- **Naming**: snake_case for variables and functions
- **Testing**: pytest with test_i_can_xxx / test_i_cannot_xxx patterns
+### Security Best Practices
+- **Password Storage**: Never store plain text passwords, always use bcrypt hashing
+- **JWT Secrets**: Use strong, randomly generated secret keys in production
+- **Token Expiration**: 24-hour expiration with secure signature validation
+- **Role Validation**: Server-side role checking for all protected endpoints
+
### Dependencies Management
- **Package Manager**: pip (standard)
- **External Dependencies**: Listed in each service's requirements.txt
@@ -228,15 +304,20 @@ docker-compose logs -f worker
### Testing Strategy
- All code must be testable
-- Unit tests for each processing function
-- Integration tests for file processing workflow
+- Unit tests for each authentication and user management function
+- Integration tests for complete authentication flow
- Tests validated before implementation
### Critical Architecture Decisions Made
-1. **Option Selected**: Single FastAPI service handles both API and file watching
-2. **Celery with Redis**: Chosen over other async patterns for scalability
-3. **EasyOCR Preferred**: Selected over Tesseract for modern OCR needs
-4. **Container Development**: Hot-reload setup required for development workflow
+1. **JWT Authentication**: Simple token-based auth with 24-hour expiration
+2. **Role-Based Access**: Admin/User roles for granular permissions
+3. **bcrypt Password Hashing**: Industry-standard password security
+4. **MongoDB User Storage**: Centralized user management in main database
+5. **Auto Admin Creation**: Automatic setup for first-time deployment
+6. **Single FastAPI Service**: Handles both API and file watching with authentication
+7. **Celery with Redis**: Chosen over other async patterns for scalability
+8. **EasyOCR Preferred**: Selected over Tesseract for modern OCR needs
+9. **Container Development**: Hot-reload setup required for development workflow
### Development Process Requirements
1. **Collaborative Validation**: All options must be explained before coding
@@ -245,11 +326,25 @@ docker-compose logs -f worker
4. **Error Handling**: Clear problem explanation required before proposing fixes
### Next Implementation Steps
-1. Create docker-compose.yml with all services
-2. Implement basic FastAPI service structure
-3. Add watchdog file monitoring
-4. Create Celery task structure
-5. Implement document processing tasks
-6. Build React monitoring interface
+1. ✅ Create docker-compose.yml with all services
+2. ✅ Define user management and authentication architecture
+3. Implement user models and authentication services
+4. Create protected API routes for user management
+5. Add automatic admin user creation
+6. Implement basic FastAPI service structure
+7. Add watchdog file monitoring
+8. Create Celery task structure
+9. Implement document processing tasks
+10. Build React monitoring interface with authentication
-"""
\ No newline at end of file
+### prochaines étapes
+MongoDB CRUD
+Nous devons absolument mocker MongoDB pour les tests unitaires avec pytest-mock
+Fichiers à créer:
+* app/models/auht.py => déjà fait
+* app/models/user.py => déjà fait
+* app/database/connection.py
+ * Utilise les settings pour l'URL MongoDB. Il faut créer un fichier de configuration (app/config/settings.py)
+ * Fonction get_database() + gestion des erreurs
+ * Configuration via variables d'environnement
+* app/database/repositories/user_repository.py
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index b2f9cab..b896ce5 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,21 +1,30 @@
amqp==5.3.1
annotated-types==0.7.0
anyio==4.10.0
+bcrypt==4.3.0
billiard==4.2.1
+bson==0.5.10
celery==5.5.3
click==8.2.1
click-didyoumean==0.3.1
click-plugins==1.1.1.2
click-repl==0.3.0
+dnspython==2.8.0
+email-validator==2.3.0
fastapi==0.116.1
h11==0.16.0
httptools==0.6.4
idna==3.10
+iniconfig==2.1.0
kombu==5.5.4
packaging==25.0
+pluggy==1.6.0
prompt_toolkit==3.0.52
pydantic==2.11.9
pydantic_core==2.33.2
+Pygments==2.19.2
+pymongo==4.15.0
+pytest==8.4.2
python-dateutil==2.9.0.post0
python-dotenv==1.1.1
PyYAML==6.0.2
diff --git a/src/file-processor/app/config/__init__.py b/src/file-processor/app/config/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/file-processor/app/config/settings.py b/src/file-processor/app/config/settings.py
new file mode 100644
index 0000000..68720c1
--- /dev/null
+++ b/src/file-processor/app/config/settings.py
@@ -0,0 +1,85 @@
+"""
+Application configuration settings.
+
+This module handles environment variables and application configuration
+using simple os.getenv() approach without external validation libraries.
+"""
+
+import os
+from typing import Optional
+
+
+def get_mongodb_url() -> str:
+ """
+ Get MongoDB connection URL from environment variables.
+
+ Returns:
+ str: MongoDB connection URL
+
+ Returns default localhost URL if MONGODB_URL not set.
+ """
+ return os.getenv("MONGODB_URL", "mongodb://localhost:27017")
+
+
+def get_mongodb_database_name() -> str:
+ """
+ Get MongoDB database name from environment variables.
+
+ Returns:
+ str: MongoDB database name
+ """
+ return os.getenv("MONGODB_DATABASE", "mydocmanager")
+
+
+def get_jwt_secret_key() -> str:
+ """
+ Get JWT secret key from environment variables.
+
+ Returns:
+ str: JWT secret key
+
+ Raises:
+ ValueError: If JWT_SECRET is not set in production
+ """
+ secret = os.getenv("JWT_SECRET")
+ if not secret:
+ # For development, provide a default key with warning
+ if os.getenv("ENVIRONMENT", "development") == "development":
+ print("WARNING: Using default JWT secret key for development")
+ return "dev-secret-key-change-in-production"
+ else:
+ raise ValueError("JWT_SECRET environment variable must be set in production")
+ return secret
+
+
+def get_jwt_algorithm() -> str:
+ """
+ Get JWT algorithm from environment variables.
+
+ Returns:
+ str: JWT algorithm (default: HS256)
+ """
+ return os.getenv("JWT_ALGORITHM", "HS256")
+
+
+def get_jwt_expire_hours() -> int:
+ """
+ Get JWT token expiration time in hours from environment variables.
+
+ Returns:
+ int: JWT expiration time in hours (default: 24)
+ """
+ try:
+ return int(os.getenv("JWT_EXPIRE_HOURS", "24"))
+ except ValueError:
+ return 24
+
+
+def is_development_environment() -> bool:
+ """
+ Check if running in development environment.
+
+ Returns:
+ bool: True if development environment
+ """
+ return os.getenv("ENVIRONMENT", "development").lower() == "development"
\ No newline at end of file
diff --git a/src/file-processor/app/database/__init__.py b/src/file-processor/app/database/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/file-processor/app/database/connection.py b/src/file-processor/app/database/connection.py
new file mode 100644
index 0000000..b6cc000
--- /dev/null
+++ b/src/file-processor/app/database/connection.py
@@ -0,0 +1,125 @@
+"""
+MongoDB database connection management.
+
+This module handles MongoDB connection with fail-fast approach.
+The application will terminate if MongoDB is not accessible at startup.
+"""
+
+import sys
+from typing import Optional
+from pymongo import MongoClient
+from pymongo.database import Database
+from pymongo.errors import ConnectionFailure, ServerSelectionTimeoutError
+
+from ..config.settings import get_mongodb_url, get_mongodb_database_name
+
+# Global variables for singleton pattern
+_client: Optional[MongoClient] = None
+_database: Optional[Database] = None
+
+
+def create_mongodb_client() -> MongoClient:
+ """
+ Create MongoDB client with connection validation.
+
+ Returns:
+ MongoClient: Connected MongoDB client
+
+ Raises:
+ SystemExit: If connection fails (fail-fast approach)
+ """
+ mongodb_url = get_mongodb_url()
+
+ try:
+ # Create client with short timeout for fail-fast behavior
+ client = MongoClient(
+ mongodb_url,
+ serverSelectionTimeoutMS=5000, # 5 seconds timeout
+ connectTimeoutMS=5000,
+ socketTimeoutMS=5000
+ )
+
+ # Test connection by running admin command
+ client.admin.command('ping')
+
+ print(f"Successfully connected to MongoDB at {mongodb_url}")
+ return client
+
+ except (ConnectionFailure, ServerSelectionTimeoutError) as e:
+ print(f"ERROR: Failed to connect to MongoDB at {mongodb_url}")
+ print(f"Connection error: {str(e)}")
+ print("MongoDB is required for this application. Please ensure MongoDB is running and accessible.")
+ sys.exit(1)
+ except Exception as e:
+ print(f"ERROR: Unexpected error connecting to MongoDB: {str(e)}")
+ sys.exit(1)
+
+
+def get_database() -> Database:
+ """
+ Get MongoDB database instance (singleton pattern).
+
+ Returns:
+ Database: MongoDB database instance
+
+ This function implements singleton pattern to ensure only one
+ database connection is created throughout the application lifecycle.
+ """
+ global _client, _database
+
+ if _database is None:
+ if _client is None:
+ _client = create_mongodb_client()
+
+ database_name = get_mongodb_database_name()
+ _database = _client[database_name]
+ print(f"Connected to database: {database_name}")
+
+ return _database
+
+
+def close_database_connection():
+ """
+ Close MongoDB database connection.
+
+ This function should be called during application shutdown
+ to properly close the database connection.
+ """
+ global _client, _database
+
+ if _client is not None:
+ _client.close()
+ _client = None
+ _database = None
+ print("MongoDB connection closed")
+
+
+def get_mongodb_client() -> Optional[MongoClient]:
+ """
+ Get the raw MongoDB client instance.
+
+ Returns:
+ MongoClient or None: MongoDB client instance if connected
+
+ This is primarily for testing purposes or advanced use cases
+ where direct client access is needed.
+ """
+ return _client
+
+
+def test_database_connection() -> bool:
+ """
+ Test if database connection is working.
+
+ Returns:
+ bool: True if connection is working, False otherwise
+
+ This function can be used for health checks.
+ """
+ try:
+ db = get_database()
+ # Simple operation to test connection
+ db.command('ping')
+ return True
+ except Exception:
+ return False
\ No newline at end of file
diff --git a/src/file-processor/app/database/repositories/__init__.py b/src/file-processor/app/database/repositories/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/file-processor/app/database/repositories/user_repository.py b/src/file-processor/app/database/repositories/user_repository.py
new file mode 100644
index 0000000..a52a925
--- /dev/null
+++ b/src/file-processor/app/database/repositories/user_repository.py
@@ -0,0 +1,221 @@
+"""
+User repository for MongoDB operations.
+
+This module implements the repository pattern for user CRUD operations
+with dependency injection of the database connection.
+"""
+
+from typing import Optional, List, Dict, Any
+from datetime import datetime
+from bson import ObjectId
+from pymongo.database import Database
+from pymongo.errors import DuplicateKeyError
+from pymongo.collection import Collection
+
+from app.models.user import UserCreate, UserInDB, UserUpdate
+
+
+class UserRepository:
+ """
+ Repository class for user CRUD operations in MongoDB.
+
+ This class handles all database operations related to users,
+ following the repository pattern with dependency injection.
+ """
+
+ def __init__(self, database: Database):
+ """
+ Initialize repository with database dependency.
+
+ Args:
+ database (Database): MongoDB database instance
+ """
+ self.db = database
+ self.collection: Collection = database.users
+
+ # Create unique index on username for duplicate prevention
+ self._ensure_indexes()
+
+ def _ensure_indexes(self):
+ """
+ Ensure required database indexes exist.
+
+ Creates unique index on username field to prevent duplicates.
+ """
+ try:
+ self.collection.create_index("username", unique=True)
+ except Exception:
+ # Index might already exist, ignore error
+ pass
+
+ def create_user(self, user_data: UserCreate) -> UserInDB:
+ """
+ Create a new user in the database.
+
+ Args:
+ user_data (UserCreate): User creation data
+
+ Returns:
+ UserInDB: Created user with database ID
+
+ Raises:
+ DuplicateKeyError: If username already exists
+ """
+ user_dict = {
+ "username": user_data.username,
+ "email": user_data.email,
+ "hashed_password": user_data.hashed_password,
+ "role": user_data.role,
+ "is_active": user_data.is_active,
+ "created_at": datetime.utcnow(),
+ "updated_at": datetime.utcnow()
+ }
+
+ try:
+ result = self.collection.insert_one(user_dict)
+ user_dict["_id"] = result.inserted_id
+ return UserInDB(**user_dict)
+ except DuplicateKeyError:
+ raise DuplicateKeyError(f"User with username '{user_data.username}' already exists")
+
+ def find_user_by_username(self, username: str) -> Optional[UserInDB]:
+ """
+ Find user by username.
+
+ Args:
+ username (str): Username to search for
+
+ Returns:
+ UserInDB or None: User if found, None otherwise
+ """
+ user_doc = self.collection.find_one({"username": username})
+ if user_doc:
+ return UserInDB(**user_doc)
+ return None
+
+ def find_user_by_id(self, user_id: str) -> Optional[UserInDB]:
+ """
+ Find user by ID.
+
+ Args:
+ user_id (str): User ID to search for
+
+ Returns:
+ UserInDB or None: User if found, None otherwise
+ """
+ try:
+ object_id = ObjectId(user_id)
+ user_doc = self.collection.find_one({"_id": object_id})
+ if user_doc:
+ return UserInDB(**user_doc)
+ except Exception:
+ # Invalid ObjectId format
+ pass
+ return None
+
+ def find_user_by_email(self, email: str) -> Optional[UserInDB]:
+ """
+ Find user by email address.
+
+ Args:
+ email (str): Email address to search for
+
+ Returns:
+ UserInDB or None: User if found, None otherwise
+ """
+ user_doc = self.collection.find_one({"email": email})
+ if user_doc:
+ return UserInDB(**user_doc)
+ return None
+
+ def update_user(self, user_id: str, user_update: UserUpdate) -> Optional[UserInDB]:
+ """
+ Update user information.
+
+ Args:
+ user_id (str): User ID to update
+ user_update (UserUpdate): Updated user data
+
+ Returns:
+ UserInDB or None: Updated user if found, None otherwise
+ """
+ try:
+ object_id = ObjectId(user_id)
+
+ # Build update document with only provided fields
+ update_data = {"updated_at": datetime.utcnow()}
+
+ if user_update.email is not None:
+ update_data["email"] = user_update.email
+ if user_update.hashed_password is not None:
+ update_data["hashed_password"] = user_update.hashed_password
+ if user_update.role is not None:
+ update_data["role"] = user_update.role
+ if user_update.is_active is not None:
+ update_data["is_active"] = user_update.is_active
+
+ result = self.collection.update_one(
+ {"_id": object_id},
+ {"$set": update_data}
+ )
+
+ if result.matched_count > 0:
+ return self.find_user_by_id(user_id)
+
+ except Exception:
+ # Invalid ObjectId format or other errors
+ pass
+ return None
+
+ def delete_user(self, user_id: str) -> bool:
+ """
+ Delete user from database.
+
+ Args:
+ user_id (str): User ID to delete
+
+ Returns:
+ bool: True if user was deleted, False otherwise
+ """
+ try:
+ object_id = ObjectId(user_id)
+ result = self.collection.delete_one({"_id": object_id})
+ return result.deleted_count > 0
+ except Exception:
+ # Invalid ObjectId format
+ return False
+
+ def list_users(self, skip: int = 0, limit: int = 100) -> List[UserInDB]:
+ """
+ List users with pagination.
+
+ Args:
+ skip (int): Number of users to skip (default: 0)
+ limit (int): Maximum number of users to return (default: 100)
+
+ Returns:
+ List[UserInDB]: List of users
+ """
+ cursor = self.collection.find().skip(skip).limit(limit)
+ return [UserInDB(**user_doc) for user_doc in cursor]
+
+ def count_users(self) -> int:
+ """
+ Count total number of users.
+
+ Returns:
+ int: Total number of users in database
+ """
+ return self.collection.count_documents({})
+
+ def user_exists(self, username: str) -> bool:
+ """
+ Check if user exists by username.
+
+ Args:
+ username (str): Username to check
+
+ Returns:
+ bool: True if user exists, False otherwise
+ """
+ return self.collection.count_documents({"username": username}) > 0
\ No newline at end of file
diff --git a/src/file-processor/app/models/__init__.py b/src/file-processor/app/models/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/file-processor/app/models/auth.py b/src/file-processor/app/models/auth.py
new file mode 100644
index 0000000..e40644a
--- /dev/null
+++ b/src/file-processor/app/models/auth.py
@@ -0,0 +1,14 @@
+"""
+Authentication models and enums for user management.
+
+Contains user roles enumeration and authentication-related Pydantic models.
+"""
+
+from enum import Enum
+
+
+class UserRole(str, Enum):
+ """User roles enumeration with string values."""
+
+ USER = "user"
+ ADMIN = "admin"
\ No newline at end of file
diff --git a/src/file-processor/app/models/user.py b/src/file-processor/app/models/user.py
new file mode 100644
index 0000000..6e2b6c6
--- /dev/null
+++ b/src/file-processor/app/models/user.py
@@ -0,0 +1,179 @@
+"""
+User models and validation for the MyDocManager application.
+
+Contains Pydantic models for user creation, updates, database representation,
+and API responses with proper validation and type safety.
+"""
+
+import re
+from datetime import datetime
+from typing import Optional, Any
+from bson import ObjectId
+from pydantic import BaseModel, Field, field_validator, EmailStr
+from pydantic_core import core_schema
+
+from app.models.auth import UserRole
+
+
+class PyObjectId(ObjectId):
+ """Custom ObjectId type for Pydantic v2 compatibility."""
+
+ @classmethod
+ def __get_pydantic_core_schema__(
+ cls, source_type: Any, handler
+ ) -> core_schema.CoreSchema:
+ return core_schema.json_or_python_schema(
+ json_schema=core_schema.str_schema(),
+ python_schema=core_schema.union_schema([
+ core_schema.is_instance_schema(ObjectId),
+ core_schema.chain_schema([
+ core_schema.str_schema(),
+ core_schema.no_info_plain_validator_function(cls.validate),
+ ])
+ ]),
+ serialization=core_schema.plain_serializer_function_ser_schema(
+ lambda x: str(x)
+ ),
+ )
+
+ @classmethod
+ def validate(cls, v):
+ if not ObjectId.is_valid(v):
+ raise ValueError("Invalid ObjectId")
+ return ObjectId(v)
+
+
+def validate_password_strength(password: str) -> str:
+ """
+ Validate password meets security requirements.
+
+ Requirements:
+ - At least 8 characters long
+ - Contains at least one uppercase letter
+ - Contains at least one lowercase letter
+ - Contains at least one digit
+ - Contains at least one special character
+
+ Args:
+ password: The password string to validate
+
+ Returns:
+ str: The validated password
+
+ Raises:
+ ValueError: If password doesn't meet requirements
+ """
+ if len(password) < 8:
+ raise ValueError("Password must be at least 8 characters long")
+
+ if not re.search(r'[A-Z]', password):
+ raise ValueError("Password must contain at least one uppercase letter")
+
+ if not re.search(r'[a-z]', password):
+ raise ValueError("Password must contain at least one lowercase letter")
+
+ if not re.search(r'\d', password):
+ raise ValueError("Password must contain at least one digit")
+
+ if not re.search(r'[!@#$%^&*()_+\-=\[\]{};:"\\|,.<>\/?]', password):
+ raise ValueError("Password must contain at least one special character")
+
+ return password
+
+
+def validate_username_not_empty(username: str) -> str:
+ """
+ Validate username is not empty or whitespace only.
+
+ Args:
+ username: The username string to validate
+
+ Returns:
+ str: The validated username
+
+ Raises:
+ ValueError: If username is empty or whitespace only
+ """
+ if not username or not username.strip():
+ raise ValueError("Username cannot be empty or whitespace only")
+
+ return username.strip()
+
+
+class UserCreate(BaseModel):
+ """Model for creating a new user."""
+
+ username: str
+ email: EmailStr
+ password: str
+ role: UserRole = UserRole.USER
+
+ @field_validator('username')
+ @classmethod
+ def validate_username(cls, v):
+ return validate_username_not_empty(v)
+
+ @field_validator('password')
+ @classmethod
+ def validate_password(cls, v):
+ return validate_password_strength(v)
+
+
+class UserUpdate(BaseModel):
+ """Model for updating an existing user."""
+
+ username: Optional[str] = None
+ email: Optional[EmailStr] = None
+ password: Optional[str] = None
+ role: Optional[UserRole] = None
+
+ @field_validator('username')
+ @classmethod
+ def validate_username(cls, v):
+ if v is not None:
+ return validate_username_not_empty(v)
+ return v
+
+ @field_validator('password')
+ @classmethod
+ def validate_password(cls, v):
+ if v is not None:
+ return validate_password_strength(v)
+ return v
+
+
+class UserInDB(BaseModel):
+ """Model for user data stored in database."""
+
+ id: PyObjectId = Field(default_factory=PyObjectId, alias="_id")
+ username: str
+ email: str
+ password_hash: str
+ role: UserRole
+ is_active: bool = True
+ created_at: datetime
+ updated_at: datetime
+
+ model_config = {
+ "populate_by_name": True,
+ "arbitrary_types_allowed": True,
+ "json_encoders": {ObjectId: str}
+ }
+
+
+class UserResponse(BaseModel):
+ """Model for user data in API responses (excludes password_hash)."""
+
+ id: PyObjectId = Field(alias="_id")
+ username: str
+ email: str
+ role: UserRole
+ is_active: bool
+ created_at: datetime
+ updated_at: datetime
+
+ model_config = {
+ "populate_by_name": True,
+ "arbitrary_types_allowed": True,
+ "json_encoders": {ObjectId: str}
+ }
\ No newline at end of file
diff --git a/src/file-processor/app/utils/__init__.py b/src/file-processor/app/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/file-processor/app/utils/security.py b/src/file-processor/app/utils/security.py
new file mode 100644
index 0000000..deda634
--- /dev/null
+++ b/src/file-processor/app/utils/security.py
@@ -0,0 +1,74 @@
+"""
+Password security utilities using bcrypt for secure password hashing.
+
+This module provides secure password hashing and verification functions
+using the bcrypt algorithm with automatic salt generation.
+"""
+
+import bcrypt
+from typing import Union
+
+
+def hash_password(password: str) -> str:
+ """
+ Hash a password using bcrypt with automatic salt generation.
+
+ Args:
+ password: The plain text password to hash
+
+ Returns:
+ The hashed password as a string
+
+ Raises:
+ ValueError: If password is empty or None
+ RuntimeError: If bcrypt hashing fails
+ """
+ if not password:
+ raise ValueError("Password cannot be empty or None")
+
+ try:
+ # Encode password to bytes and generate salt
+ password_bytes = password.encode('utf-8')
+ salt = bcrypt.gensalt()
+
+ # Hash the password
+ hashed = bcrypt.hashpw(password_bytes, salt)
+
+ # Return as string
+ return hashed.decode('utf-8')
+
+ except Exception as e:
+ raise RuntimeError(f"Failed to hash password: {str(e)}")
+
+
+def verify_password(password: str, hashed_password: str) -> bool:
+ """
+ Verify a password against its hash.
+
+ Args:
+ password: The plain text password to verify
+ hashed_password: The hashed password to verify against
+
+ Returns:
+ True if password matches the hash, False otherwise
+
+ Raises:
+ ValueError: If password or hashed_password is empty or None
+ RuntimeError: If password verification fails due to malformed hash
+ """
+ if not password or not hashed_password:
+ raise ValueError("Password and hashed_password cannot be empty or None")
+
+ try:
+ # Encode inputs to bytes
+ password_bytes = password.encode('utf-8')
+ hashed_bytes = hashed_password.encode('utf-8')
+
+ # Verify password
+ return bcrypt.checkpw(password_bytes, hashed_bytes)
+
+ except ValueError as e:
+ # bcrypt raises ValueError for malformed hashes
+ raise RuntimeError(f"Invalid hash format: {str(e)}")
+ except Exception as e:
+ raise RuntimeError(f"Failed to verify password: {str(e)}")
\ No newline at end of file
diff --git a/src/frontend/.gitignore b/src/frontend/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/src/frontend/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/src/frontend/README.md b/src/frontend/README.md
new file mode 100644
index 0000000..7059a96
--- /dev/null
+++ b/src/frontend/README.md
@@ -0,0 +1,12 @@
+# React + Vite
+
+This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
+
+Currently, two official plugins are available:
+
+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
+
+## Expanding the ESLint configuration
+
+If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
diff --git a/src/frontend/eslint.config.js b/src/frontend/eslint.config.js
new file mode 100644
index 0000000..cee1e2c
--- /dev/null
+++ b/src/frontend/eslint.config.js
@@ -0,0 +1,29 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{js,jsx}'],
+ extends: [
+ js.configs.recommended,
+ reactHooks.configs['recommended-latest'],
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ parserOptions: {
+ ecmaVersion: 'latest',
+ ecmaFeatures: { jsx: true },
+ sourceType: 'module',
+ },
+ },
+ rules: {
+ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
+ },
+ },
+])
diff --git a/src/frontend/index.html b/src/frontend/index.html
new file mode 100644
index 0000000..0c589ec
--- /dev/null
+++ b/src/frontend/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Vite + React
+
+
+
+
+
+
diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json
new file mode 100644
index 0000000..db82ae7
--- /dev/null
+++ b/src/frontend/package-lock.json
@@ -0,0 +1,2808 @@
+{
+ "name": "frontend",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "frontend",
+ "version": "0.0.0",
+ "dependencies": {
+ "react": "^19.1.1",
+ "react-dom": "^19.1.1"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.33.0",
+ "@types/react": "^19.1.10",
+ "@types/react-dom": "^19.1.7",
+ "@vitejs/plugin-react": "^5.0.0",
+ "eslint": "^9.33.0",
+ "eslint-plugin-react-hooks": "^5.2.0",
+ "eslint-plugin-react-refresh": "^0.4.20",
+ "globals": "^16.3.0",
+ "vite": "^7.1.2"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz",
+ "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
+ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.3",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-module-transforms": "^7.28.3",
+ "@babel/helpers": "^7.28.4",
+ "@babel/parser": "^7.28.4",
+ "@babel/template": "^7.27.2",
+ "@babel/traverse": "^7.28.4",
+ "@babel/types": "^7.28.4",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz",
+ "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.28.3",
+ "@babel/types": "^7.28.2",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.27.2",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
+ "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.28.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
+ "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
+ "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.4"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
+ "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.4"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz",
+ "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.3",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.28.4",
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.4",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
+ "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz",
+ "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz",
+ "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz",
+ "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz",
+ "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz",
+ "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz",
+ "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz",
+ "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz",
+ "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz",
+ "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz",
+ "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz",
+ "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz",
+ "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz",
+ "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz",
+ "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz",
+ "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz",
+ "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz",
+ "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz",
+ "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz",
+ "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz",
+ "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz",
+ "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz",
+ "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz",
+ "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz",
+ "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz",
+ "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz",
+ "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
+ "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
+ "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz",
+ "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.6",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz",
+ "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.15.2",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz",
+ "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
+ "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.35.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz",
+ "integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
+ "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz",
+ "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.15.2",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.7",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
+ "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.34",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.34.tgz",
+ "integrity": "sha512-LyAREkZHP5pMom7c24meKmJCdhf2hEyvam2q0unr3or9ydwDL+DJ8chTF6Av/RFPb3rH8UFBdMzO5MxTZW97oA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.2.tgz",
+ "integrity": "sha512-uLN8NAiFVIRKX9ZQha8wy6UUs06UNSZ32xj6giK/rmMXAgKahwExvK6SsmgU5/brh4w/nSgj8e0k3c1HBQpa0A==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.2.tgz",
+ "integrity": "sha512-oEouqQk2/zxxj22PNcGSskya+3kV0ZKH+nQxuCCOGJ4oTXBdNTbv+f/E3c74cNLeMO1S5wVWacSws10TTSB77g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.2.tgz",
+ "integrity": "sha512-OZuTVTpj3CDSIxmPgGH8en/XtirV5nfljHZ3wrNwvgkT5DQLhIKAeuFSiwtbMto6oVexV0k1F1zqURPKf5rI1Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.2.tgz",
+ "integrity": "sha512-Wa/Wn8RFkIkr1vy1k1PB//VYhLnlnn5eaJkfTQKivirOvzu5uVd2It01ukeQstMursuz7S1bU+8WW+1UPXpa8A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.2.tgz",
+ "integrity": "sha512-QkzxvH3kYN9J1w7D1A+yIMdI1pPekD+pWx7G5rXgnIlQ1TVYVC6hLl7SOV9pi5q9uIDF9AuIGkuzcbF7+fAhow==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.2.tgz",
+ "integrity": "sha512-dkYXB0c2XAS3a3jmyDkX4Jk0m7gWLFzq1C3qUnJJ38AyxIF5G/dyS4N9B30nvFseCfgtCEdbYFhk0ChoCGxPog==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.2.tgz",
+ "integrity": "sha512-9VlPY/BN3AgbukfVHAB8zNFWB/lKEuvzRo1NKev0Po8sYFKx0i+AQlCYftgEjcL43F2h9Ui1ZSdVBc4En/sP2w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.2.tgz",
+ "integrity": "sha512-+GdKWOvsifaYNlIVf07QYan1J5F141+vGm5/Y8b9uCZnG/nxoGqgCmR24mv0koIWWuqvFYnbURRqw1lv7IBINw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.2.tgz",
+ "integrity": "sha512-df0Eou14ojtUdLQdPFnymEQteENwSJAdLf5KCDrmZNsy1c3YaCNaJvYsEUHnrg+/DLBH612/R0xd3dD03uz2dg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.2.tgz",
+ "integrity": "sha512-iPeouV0UIDtz8j1YFR4OJ/zf7evjauqv7jQ/EFs0ClIyL+by++hiaDAfFipjOgyz6y6xbDvJuiU4HwpVMpRFDQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.50.2.tgz",
+ "integrity": "sha512-OL6KaNvBopLlj5fTa5D5bau4W82f+1TyTZRr2BdnfsrnQnmdxh4okMxR2DcDkJuh4KeoQZVuvHvzuD/lyLn2Kw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.2.tgz",
+ "integrity": "sha512-I21VJl1w6z/K5OTRl6aS9DDsqezEZ/yKpbqlvfHbW0CEF5IL8ATBMuUx6/mp683rKTK8thjs/0BaNrZLXetLag==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.2.tgz",
+ "integrity": "sha512-Hq6aQJT/qFFHrYMjS20nV+9SKrXL2lvFBENZoKfoTH2kKDOJqff5OSJr4x72ZaG/uUn+XmBnGhfr4lwMRrmqCQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.2.tgz",
+ "integrity": "sha512-82rBSEXRv5qtKyr0xZ/YMF531oj2AIpLZkeNYxmKNN6I2sVE9PGegN99tYDLK2fYHJITL1P2Lgb4ZXnv0PjQvw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.2.tgz",
+ "integrity": "sha512-4Q3S3Hy7pC6uaRo9gtXUTJ+EKo9AKs3BXKc2jYypEcMQ49gDPFU2P1ariX9SEtBzE5egIX6fSUmbmGazwBVF9w==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.2.tgz",
+ "integrity": "sha512-9Jie/At6qk70dNIcopcL4p+1UirusEtznpNtcq/u/C5cC4HBX7qSGsYIcG6bdxj15EYWhHiu02YvmdPzylIZlA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.2.tgz",
+ "integrity": "sha512-HPNJwxPL3EmhzeAnsWQCM3DcoqOz3/IC6de9rWfGR8ZCuEHETi9km66bH/wG3YH0V3nyzyFEGUZeL5PKyy4xvw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.2.tgz",
+ "integrity": "sha512-nMKvq6FRHSzYfKLHZ+cChowlEkR2lj/V0jYj9JnGUVPL2/mIeFGmVM2mLaFeNa5Jev7W7TovXqXIG2d39y1KYA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.2.tgz",
+ "integrity": "sha512-eFUvvnTYEKeTyHEijQKz81bLrUQOXKZqECeiWH6tb8eXXbZk+CXSG2aFrig2BQ/pjiVRj36zysjgILkqarS2YA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.2.tgz",
+ "integrity": "sha512-cBaWmXqyfRhH8zmUxK3d3sAhEWLrtMjWBRwdMMHJIXSjvjLKvv49adxiEz+FJ8AP90apSDDBx2Tyd/WylV6ikA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.2.tgz",
+ "integrity": "sha512-APwKy6YUhvZaEoHyM+9xqmTpviEI+9eL7LoCH+aLcvWYHJ663qG5zx7WzWZY+a9qkg5JtzcMyJ9z0WtQBMDmgA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "19.1.13",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz",
+ "integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.1.9",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz",
+ "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.0.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.2.tgz",
+ "integrity": "sha512-tmyFgixPZCx2+e6VO9TNITWcCQl8+Nl/E8YbAyPVv85QCc7/A3JrdfG2A8gIzvVhWuzMOVrFW1aReaNxrI6tbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.3",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.34",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.8.4",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.4.tgz",
+ "integrity": "sha512-L+YvJwGAgwJBV1p6ffpSTa2KRc69EeeYGYjRVWKs0GKrK+LON0GC0gV+rKSNtALEDvMDqkvCFq9r1r94/Gjwxw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.js"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.26.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz",
+ "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.8.3",
+ "caniuse-lite": "^1.0.30001741",
+ "electron-to-chromium": "^1.5.218",
+ "node-releases": "^2.0.21",
+ "update-browserslist-db": "^1.1.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001743",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001743.tgz",
+ "integrity": "sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.218",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.218.tgz",
+ "integrity": "sha512-uwwdN0TUHs8u6iRgN8vKeWZMRll4gBkz+QMqdS7DDe49uiK68/UX92lFb61oiFPrpYZNeZIqa4bA7O6Aiasnzg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz",
+ "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.9",
+ "@esbuild/android-arm": "0.25.9",
+ "@esbuild/android-arm64": "0.25.9",
+ "@esbuild/android-x64": "0.25.9",
+ "@esbuild/darwin-arm64": "0.25.9",
+ "@esbuild/darwin-x64": "0.25.9",
+ "@esbuild/freebsd-arm64": "0.25.9",
+ "@esbuild/freebsd-x64": "0.25.9",
+ "@esbuild/linux-arm": "0.25.9",
+ "@esbuild/linux-arm64": "0.25.9",
+ "@esbuild/linux-ia32": "0.25.9",
+ "@esbuild/linux-loong64": "0.25.9",
+ "@esbuild/linux-mips64el": "0.25.9",
+ "@esbuild/linux-ppc64": "0.25.9",
+ "@esbuild/linux-riscv64": "0.25.9",
+ "@esbuild/linux-s390x": "0.25.9",
+ "@esbuild/linux-x64": "0.25.9",
+ "@esbuild/netbsd-arm64": "0.25.9",
+ "@esbuild/netbsd-x64": "0.25.9",
+ "@esbuild/openbsd-arm64": "0.25.9",
+ "@esbuild/openbsd-x64": "0.25.9",
+ "@esbuild/openharmony-arm64": "0.25.9",
+ "@esbuild/sunos-x64": "0.25.9",
+ "@esbuild/win32-arm64": "0.25.9",
+ "@esbuild/win32-ia32": "0.25.9",
+ "@esbuild/win32-x64": "0.25.9"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.35.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.35.0.tgz",
+ "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.0",
+ "@eslint/config-helpers": "^0.3.1",
+ "@eslint/core": "^0.15.2",
+ "@eslint/eslintrc": "^3.3.1",
+ "@eslint/js": "9.35.0",
+ "@eslint/plugin-kit": "^0.3.5",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "@types/json-schema": "^7.0.15",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz",
+ "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.4.20",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz",
+ "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": ">=8.40"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "16.4.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz",
+ "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.21",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz",
+ "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.1.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
+ "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.1.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
+ "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.26.0"
+ },
+ "peerDependencies": {
+ "react": "^19.1.1"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.2.tgz",
+ "integrity": "sha512-BgLRGy7tNS9H66aIMASq1qSYbAAJV6Z6WR4QYTvj5FgF15rZ/ympT1uixHXwzbZUBDbkvqUI1KR0fH1FhMaQ9w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.50.2",
+ "@rollup/rollup-android-arm64": "4.50.2",
+ "@rollup/rollup-darwin-arm64": "4.50.2",
+ "@rollup/rollup-darwin-x64": "4.50.2",
+ "@rollup/rollup-freebsd-arm64": "4.50.2",
+ "@rollup/rollup-freebsd-x64": "4.50.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.50.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.50.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.50.2",
+ "@rollup/rollup-linux-arm64-musl": "4.50.2",
+ "@rollup/rollup-linux-loong64-gnu": "4.50.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.50.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.50.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.50.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.50.2",
+ "@rollup/rollup-linux-x64-gnu": "4.50.2",
+ "@rollup/rollup-linux-x64-musl": "4.50.2",
+ "@rollup/rollup-openharmony-arm64": "4.50.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.50.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.50.2",
+ "@rollup/rollup-win32-x64-msvc": "4.50.2",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.26.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
+ "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
+ "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "7.1.5",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz",
+ "integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/src/frontend/package.json b/src/frontend/package.json
new file mode 100644
index 0000000..c3894ae
--- /dev/null
+++ b/src/frontend/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "frontend",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "react": "^19.1.1",
+ "react-dom": "^19.1.1"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.33.0",
+ "@types/react": "^19.1.10",
+ "@types/react-dom": "^19.1.7",
+ "@vitejs/plugin-react": "^5.0.0",
+ "eslint": "^9.33.0",
+ "eslint-plugin-react-hooks": "^5.2.0",
+ "eslint-plugin-react-refresh": "^0.4.20",
+ "globals": "^16.3.0",
+ "vite": "^7.1.2"
+ }
+}
diff --git a/src/frontend/public/vite.svg b/src/frontend/public/vite.svg
new file mode 100644
index 0000000..e7b8dfb
--- /dev/null
+++ b/src/frontend/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/frontend/src/App.css b/src/frontend/src/App.css
new file mode 100644
index 0000000..b9d355d
--- /dev/null
+++ b/src/frontend/src/App.css
@@ -0,0 +1,42 @@
+#root {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 2rem;
+ text-align: center;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
diff --git a/src/frontend/src/App.jsx b/src/frontend/src/App.jsx
new file mode 100644
index 0000000..f67355a
--- /dev/null
+++ b/src/frontend/src/App.jsx
@@ -0,0 +1,35 @@
+import { useState } from 'react'
+import reactLogo from './assets/react.svg'
+import viteLogo from '/vite.svg'
+import './App.css'
+
+function App() {
+ const [count, setCount] = useState(0)
+
+ return (
+ <>
+
+ Vite + React
+
+
+
+ Edit src/App.jsx and save to test HMR
+
+
+
+ Click on the Vite and React logos to learn more
+
+ >
+ )
+}
+
+export default App
diff --git a/src/frontend/src/assets/react.svg b/src/frontend/src/assets/react.svg
new file mode 100644
index 0000000..6c87de9
--- /dev/null
+++ b/src/frontend/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/frontend/src/index.css b/src/frontend/src/index.css
new file mode 100644
index 0000000..08a3ac9
--- /dev/null
+++ b/src/frontend/src/index.css
@@ -0,0 +1,68 @@
+:root {
+ font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+a {
+ font-weight: 500;
+ color: #646cff;
+ text-decoration: inherit;
+}
+a:hover {
+ color: #535bf2;
+}
+
+body {
+ margin: 0;
+ display: flex;
+ place-items: center;
+ min-width: 320px;
+ min-height: 100vh;
+}
+
+h1 {
+ font-size: 3.2em;
+ line-height: 1.1;
+}
+
+button {
+ border-radius: 8px;
+ border: 1px solid transparent;
+ padding: 0.6em 1.2em;
+ font-size: 1em;
+ font-weight: 500;
+ font-family: inherit;
+ background-color: #1a1a1a;
+ cursor: pointer;
+ transition: border-color 0.25s;
+}
+button:hover {
+ border-color: #646cff;
+}
+button:focus,
+button:focus-visible {
+ outline: 4px auto -webkit-focus-ring-color;
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ color: #213547;
+ background-color: #ffffff;
+ }
+ a:hover {
+ color: #747bff;
+ }
+ button {
+ background-color: #f9f9f9;
+ }
+}
diff --git a/src/frontend/src/main.jsx b/src/frontend/src/main.jsx
new file mode 100644
index 0000000..b9a1a6d
--- /dev/null
+++ b/src/frontend/src/main.jsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import './index.css'
+import App from './App.jsx'
+
+createRoot(document.getElementById('root')).render(
+
+
+ ,
+)
diff --git a/src/frontend/vite.config.js b/src/frontend/vite.config.js
new file mode 100644
index 0000000..8b0f57b
--- /dev/null
+++ b/src/frontend/vite.config.js
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react()],
+})
diff --git a/tests/test_connection.py b/tests/test_connection.py
new file mode 100644
index 0000000..ae494b4
--- /dev/null
+++ b/tests/test_connection.py
@@ -0,0 +1,194 @@
+"""
+Unit tests for MongoDB database connection module.
+
+Tests the database connection functionality with mocking
+to avoid requiring actual MongoDB instance during tests.
+"""
+
+import pytest
+from unittest.mock import Mock, patch, MagicMock
+from pymongo.errors import ConnectionFailure, ServerSelectionTimeoutError
+
+from app.database.connection import (
+ create_mongodb_client,
+ get_database,
+ close_database_connection,
+ get_mongodb_client,
+ test_database_connection
+)
+
+
+def test_i_can_get_database_connection():
+ """Test successful database connection creation."""
+ mock_client = Mock()
+ mock_database = Mock()
+ mock_client.__getitem__.return_value = mock_database
+
+ with patch('app.database.connection.MongoClient', return_value=mock_client):
+ with patch('app.database.connection.get_mongodb_url', return_value="mongodb://localhost:27017"):
+ with patch('app.database.connection.get_mongodb_database_name', return_value="testdb"):
+ # Reset global variables
+ import app.database.connection
+ app.database.connection._client = None
+ app.database.connection._database = None
+
+ result = get_database()
+
+ assert result == mock_database
+ mock_client.admin.command.assert_called_with('ping')
+
+
+def test_i_cannot_connect_to_invalid_mongodb_url():
+ """Test fail-fast behavior with invalid MongoDB URL."""
+ mock_client = Mock()
+ mock_client.admin.command.side_effect = ConnectionFailure("Connection failed")
+
+ with patch('app.database.connection.MongoClient', return_value=mock_client):
+ with patch('app.database.connection.get_mongodb_url', return_value="mongodb://invalid:27017"):
+ with pytest.raises(SystemExit) as exc_info:
+ create_mongodb_client()
+
+ assert exc_info.value.code == 1
+
+
+def test_i_cannot_connect_with_server_selection_timeout():
+ """Test fail-fast behavior with server selection timeout."""
+ mock_client = Mock()
+ mock_client.admin.command.side_effect = ServerSelectionTimeoutError("Timeout")
+
+ with patch('app.database.connection.MongoClient', return_value=mock_client):
+ with patch('app.database.connection.get_mongodb_url', return_value="mongodb://timeout:27017"):
+ with pytest.raises(SystemExit) as exc_info:
+ create_mongodb_client()
+
+ assert exc_info.value.code == 1
+
+
+def test_i_cannot_connect_with_unexpected_error():
+ """Test fail-fast behavior with unexpected connection error."""
+ with patch('app.database.connection.MongoClient', side_effect=Exception("Unexpected error")):
+ with patch('app.database.connection.get_mongodb_url', return_value="mongodb://error:27017"):
+ with pytest.raises(SystemExit) as exc_info:
+ create_mongodb_client()
+
+ assert exc_info.value.code == 1
+
+
+def test_i_can_get_database_singleton():
+ """Test that get_database returns the same instance (singleton pattern)."""
+ mock_client = Mock()
+ mock_database = Mock()
+ mock_client.__getitem__.return_value = mock_database
+
+ with patch('app.database.connection.MongoClient', return_value=mock_client):
+ with patch('app.database.connection.get_mongodb_url', return_value="mongodb://localhost:27017"):
+ with patch('app.database.connection.get_mongodb_database_name', return_value="testdb"):
+ # Reset global variables
+ import app.database.connection
+ app.database.connection._client = None
+ app.database.connection._database = None
+
+ # First call
+ db1 = get_database()
+ # Second call
+ db2 = get_database()
+
+ assert db1 is db2
+ # MongoClient should be called only once
+ assert mock_client.admin.command.call_count == 1
+
+
+def test_i_can_close_database_connection():
+ """Test closing database connection."""
+ mock_client = Mock()
+ mock_database = Mock()
+ mock_client.__getitem__.return_value = mock_database
+
+ with patch('app.database.connection.MongoClient', return_value=mock_client):
+ with patch('app.database.connection.get_mongodb_url', return_value="mongodb://localhost:27017"):
+ with patch('app.database.connection.get_mongodb_database_name', return_value="testdb"):
+ # Reset global variables
+ import app.database.connection
+ app.database.connection._client = None
+ app.database.connection._database = None
+
+ # Create connection
+ get_database()
+
+ # Close connection
+ close_database_connection()
+
+ mock_client.close.assert_called_once()
+ assert app.database.connection._client is None
+ assert app.database.connection._database is None
+
+
+def test_i_can_get_mongodb_client():
+ """Test getting raw MongoDB client instance."""
+ mock_client = Mock()
+ mock_database = Mock()
+ mock_client.__getitem__.return_value = mock_database
+
+ with patch('app.database.connection.MongoClient', return_value=mock_client):
+ with patch('app.database.connection.get_mongodb_url', return_value="mongodb://localhost:27017"):
+ with patch('app.database.connection.get_mongodb_database_name', return_value="testdb"):
+ # Reset global variables
+ import app.database.connection
+ app.database.connection._client = None
+ app.database.connection._database = None
+
+ # Create connection first
+ get_database()
+
+ # Get client
+ result = get_mongodb_client()
+
+ assert result == mock_client
+
+
+def test_i_can_get_none_mongodb_client_when_not_connected():
+ """Test getting MongoDB client returns None when not connected."""
+ # Reset global variables
+ import app.database.connection
+ app.database.connection._client = None
+ app.database.connection._database = None
+
+ result = get_mongodb_client()
+ assert result is None
+
+
+def test_i_can_test_database_connection_success():
+ """Test database connection health check - success case."""
+ mock_database = Mock()
+ mock_database.command.return_value = True
+
+ with patch('app.database.connection.get_database', return_value=mock_database):
+ result = test_database_connection()
+
+ assert result is True
+ mock_database.command.assert_called_with('ping')
+
+
+def test_i_cannot_test_database_connection_failure():
+ """Test database connection health check - failure case."""
+ mock_database = Mock()
+ mock_database.command.side_effect = Exception("Connection error")
+
+ with patch('app.database.connection.get_database', return_value=mock_database):
+ result = test_database_connection()
+
+ assert result is False
+
+
+def test_i_can_close_connection_when_no_client():
+ """Test closing connection when no client exists (should not raise error)."""
+ # Reset global variables
+ import app.database.connection
+ app.database.connection._client = None
+ app.database.connection._database = None
+
+ # Should not raise any exception
+ close_database_connection()
+
+ assert app.database.connection._client is None
+ assert app.database.connection._database is None
\ No newline at end of file
diff --git a/tests/test_security.py b/tests/test_security.py
new file mode 100644
index 0000000..ebe5c1a
--- /dev/null
+++ b/tests/test_security.py
@@ -0,0 +1,105 @@
+"""
+Unit tests for password security utilities.
+
+Tests the bcrypt-based password hashing and verification functions
+including edge cases and error handling.
+"""
+
+import pytest
+from app.utils.security import hash_password, verify_password
+
+
+def test_i_can_hash_password():
+ """Test that a password is correctly hashed and different from original."""
+ password = "my_secure_password"
+ hashed = hash_password(password)
+
+ # Hash should be different from original password
+ assert hashed != password
+
+ # Hash should be a non-empty string
+ assert isinstance(hashed, str)
+ assert len(hashed) > 0
+
+ # Hash should start with bcrypt identifier
+ assert hashed.startswith("$2b$")
+
+
+def test_same_password_generates_different_hashes():
+ """Test that the salt generates different hashes for the same password."""
+ password = "identical_password"
+
+ hash1 = hash_password(password)
+ hash2 = hash_password(password)
+
+ # Same password should generate different hashes due to salt
+ assert hash1 != hash2
+
+ # But both should be valid bcrypt hashes
+ assert hash1.startswith("$2b$")
+ assert hash2.startswith("$2b$")
+
+
+def test_i_can_verify_correct_password():
+ """Test that a correct password is validated against its hash."""
+ password = "correct_password"
+ hashed = hash_password(password)
+
+ # Correct password should verify successfully
+ assert verify_password(password, hashed) is True
+
+
+def test_i_cannot_verify_incorrect_password():
+ """Test that an incorrect password is rejected."""
+ password = "correct_password"
+ wrong_password = "wrong_password"
+ hashed = hash_password(password)
+
+ # Wrong password should fail verification
+ assert verify_password(wrong_password, hashed) is False
+
+
+def test_i_cannot_hash_empty_password():
+ """Test that empty passwords are rejected during hashing."""
+ # Empty string should raise ValueError
+ with pytest.raises(ValueError, match="Password cannot be empty or None"):
+ hash_password("")
+
+ # None should raise ValueError
+ with pytest.raises(ValueError, match="Password cannot be empty or None"):
+ hash_password(None)
+
+
+def test_i_cannot_verify_with_malformed_hash():
+ """Test that malformed hashes are rejected during verification."""
+ password = "test_password"
+ malformed_hash = "not_a_valid_bcrypt_hash"
+
+ # Malformed hash should raise RuntimeError
+ with pytest.raises(RuntimeError, match="Invalid hash format"):
+ verify_password(password, malformed_hash)
+
+
+def test_i_cannot_verify_with_none_values():
+ """Test that None values are rejected during verification."""
+ password = "test_password"
+ hashed = hash_password(password)
+
+ # None password should raise ValueError
+ with pytest.raises(ValueError, match="Password and hashed_password cannot be empty or None"):
+ verify_password(None, hashed)
+
+ # None hash should raise ValueError
+ with pytest.raises(ValueError, match="Password and hashed_password cannot be empty or None"):
+ verify_password(password, None)
+
+ # Both None should raise ValueError
+ with pytest.raises(ValueError, match="Password and hashed_password cannot be empty or None"):
+ verify_password(None, None)
+
+ # Empty strings should also raise ValueError
+ with pytest.raises(ValueError, match="Password and hashed_password cannot be empty or None"):
+ verify_password("", hashed)
+
+ with pytest.raises(ValueError, match="Password and hashed_password cannot be empty or None"):
+ verify_password(password, "")
\ No newline at end of file
diff --git a/tests/test_settings.py b/tests/test_settings.py
new file mode 100644
index 0000000..89331ea
--- /dev/null
+++ b/tests/test_settings.py
@@ -0,0 +1,141 @@
+"""
+Unit tests for configuration settings module.
+
+Tests the environment variable loading and default values
+for application configuration.
+"""
+
+import os
+import pytest
+from unittest.mock import patch
+
+from app.config.settings import (
+ get_mongodb_url,
+ get_mongodb_database_name,
+ get_jwt_secret_key,
+ get_jwt_algorithm,
+ get_jwt_expire_hours,
+ is_development_environment
+)
+
+
+def test_i_can_load_mongodb_url_from_env():
+ """Test loading MongoDB URL from environment variable."""
+ test_url = "mongodb://test-server:27017"
+
+ with patch.dict(os.environ, {"MONGODB_URL": test_url}):
+ result = get_mongodb_url()
+ assert result == test_url
+
+
+def test_i_can_use_default_mongodb_url():
+ """Test default MongoDB URL when environment variable is not set."""
+ with patch.dict(os.environ, {}, clear=True):
+ result = get_mongodb_url()
+ assert result == "mongodb://localhost:27017"
+
+
+def test_i_can_load_mongodb_database_name_from_env():
+ """Test loading MongoDB database name from environment variable."""
+ test_db_name = "test_database"
+
+ with patch.dict(os.environ, {"MONGODB_DATABASE": test_db_name}):
+ result = get_mongodb_database_name()
+ assert result == test_db_name
+
+
+def test_i_can_use_default_mongodb_database_name():
+ """Test default MongoDB database name when environment variable is not set."""
+ with patch.dict(os.environ, {}, clear=True):
+ result = get_mongodb_database_name()
+ assert result == "mydocmanager"
+
+
+def test_i_can_load_jwt_secret_from_env():
+ """Test loading JWT secret from environment variable."""
+ test_secret = "super-secret-key-123"
+
+ with patch.dict(os.environ, {"JWT_SECRET": test_secret}):
+ result = get_jwt_secret_key()
+ assert result == test_secret
+
+
+def test_i_can_use_default_jwt_secret_in_development():
+ """Test default JWT secret in development environment."""
+ with patch.dict(os.environ, {"ENVIRONMENT": "development"}, clear=True):
+ result = get_jwt_secret_key()
+ assert result == "dev-secret-key-change-in-production"
+
+
+def test_i_cannot_get_jwt_secret_in_production_without_env():
+ """Test that JWT secret raises error in production without environment variable."""
+ with patch.dict(os.environ, {"ENVIRONMENT": "production"}, clear=True):
+ with pytest.raises(ValueError, match="JWT_SECRET environment variable must be set in production"):
+ get_jwt_secret_key()
+
+
+def test_i_can_load_jwt_algorithm_from_env():
+ """Test loading JWT algorithm from environment variable."""
+ test_algorithm = "RS256"
+
+ with patch.dict(os.environ, {"JWT_ALGORITHM": test_algorithm}):
+ result = get_jwt_algorithm()
+ assert result == test_algorithm
+
+
+def test_i_can_use_default_jwt_algorithm():
+ """Test default JWT algorithm when environment variable is not set."""
+ with patch.dict(os.environ, {}, clear=True):
+ result = get_jwt_algorithm()
+ assert result == "HS256"
+
+
+def test_i_can_load_jwt_expire_hours_from_env():
+ """Test loading JWT expiration hours from environment variable."""
+ test_hours = "48"
+
+ with patch.dict(os.environ, {"JWT_EXPIRE_HOURS": test_hours}):
+ result = get_jwt_expire_hours()
+ assert result == 48
+
+
+def test_i_can_use_default_jwt_expire_hours():
+ """Test default JWT expiration hours when environment variable is not set."""
+ with patch.dict(os.environ, {}, clear=True):
+ result = get_jwt_expire_hours()
+ assert result == 24
+
+
+def test_i_can_handle_invalid_jwt_expire_hours():
+ """Test handling of invalid JWT expiration hours value."""
+ with patch.dict(os.environ, {"JWT_EXPIRE_HOURS": "invalid"}):
+ result = get_jwt_expire_hours()
+ assert result == 24 # Should fall back to default
+
+
+def test_i_can_detect_development_environment():
+ """Test detection of development environment."""
+ with patch.dict(os.environ, {"ENVIRONMENT": "development"}):
+ result = is_development_environment()
+ assert result is True
+
+
+def test_i_can_detect_production_environment():
+ """Test detection of production environment."""
+ with patch.dict(os.environ, {"ENVIRONMENT": "production"}):
+ result = is_development_environment()
+ assert result is False
+
+
+def test_i_can_use_default_development_environment():
+ """Test default environment is development."""
+ with patch.dict(os.environ, {}, clear=True):
+ result = is_development_environment()
+ assert result is True
+
+
+def test_i_can_handle_case_insensitive_environment():
+ """Test case insensitive environment detection."""
+ with patch.dict(os.environ, {"ENVIRONMENT": "DEVELOPMENT"}):
+ result = is_development_environment()
+ assert result is True
\ No newline at end of file
diff --git a/tests/test_user_models.py b/tests/test_user_models.py
new file mode 100644
index 0000000..4937181
--- /dev/null
+++ b/tests/test_user_models.py
@@ -0,0 +1,382 @@
+"""
+Unit tests for user models and validation.
+
+Tests the Pydantic models used for user creation, update, and response.
+Validates email format, password strength, and data serialization.
+"""
+
+import pytest
+from pydantic import ValidationError
+from datetime import datetime
+from bson import ObjectId
+
+from app.models.user import UserCreate, UserUpdate, UserInDB, UserResponse
+from app.models.auth import UserRole
+
+
+class TestUserCreateModel:
+ """Tests for UserCreate Pydantic model validation."""
+
+ def test_i_can_create_user_create_model(self):
+ """Test creation of valid UserCreate model with all required fields."""
+ user_data = {
+ "username": "testuser",
+ "email": "test@example.com",
+ "password": "TestPass123!",
+ "role": UserRole.USER
+ }
+
+ user = UserCreate(**user_data)
+
+ assert user.username == "testuser"
+ assert user.email == "test@example.com"
+ assert user.password == "TestPass123!"
+ assert user.role == UserRole.USER
+
+ def test_i_can_create_admin_user(self):
+ """Test creation of admin user with admin role."""
+ user_data = {
+ "username": "adminuser",
+ "email": "admin@example.com",
+ "password": "AdminPass123!",
+ "role": UserRole.ADMIN
+ }
+
+ user = UserCreate(**user_data)
+
+ assert user.role == UserRole.ADMIN
+
+ def test_i_cannot_create_user_with_invalid_email(self):
+ """Test that invalid email formats are rejected."""
+ invalid_emails = [
+ "notanemail",
+ "@example.com",
+ "test@",
+ "test.example.com",
+ "",
+ "test@.com"
+ ]
+
+ for invalid_email in invalid_emails:
+ user_data = {
+ "username": "testuser",
+ "email": invalid_email,
+ "password": "TestPass123!",
+ "role": UserRole.USER
+ }
+
+ with pytest.raises(ValidationError) as exc_info:
+ UserCreate(**user_data)
+
+ assert "email" in str(exc_info.value).lower()
+
+ def test_i_cannot_create_user_with_short_password(self):
+ """Test that passwords shorter than 8 characters are rejected."""
+ short_passwords = [
+ "Test1!", # 6 chars
+ "Te1!", # 4 chars
+ "1234567", # 7 chars
+ "" # empty
+ ]
+
+ for short_password in short_passwords:
+ user_data = {
+ "username": "testuser",
+ "email": "test@example.com",
+ "password": short_password,
+ "role": UserRole.USER
+ }
+
+ with pytest.raises(ValidationError) as exc_info:
+ UserCreate(**user_data)
+
+ assert "password" in str(exc_info.value).lower()
+
+ def test_i_cannot_create_user_without_uppercase(self):
+ """Test that passwords without uppercase letters are rejected."""
+ passwords_without_uppercase = [
+ "testpass123!",
+ "mypassword1!",
+ "lowercase123!"
+ ]
+
+ for password in passwords_without_uppercase:
+ user_data = {
+ "username": "testuser",
+ "email": "test@example.com",
+ "password": password,
+ "role": UserRole.USER
+ }
+
+ with pytest.raises(ValidationError) as exc_info:
+ UserCreate(**user_data)
+
+ assert "password" in str(exc_info.value).lower()
+
+ def test_i_cannot_create_user_without_lowercase(self):
+ """Test that passwords without lowercase letters are rejected."""
+ passwords_without_lowercase = [
+ "TESTPASS123!",
+ "MYPASSWORD1!",
+ "UPPERCASE123!"
+ ]
+
+ for password in passwords_without_lowercase:
+ user_data = {
+ "username": "testuser",
+ "email": "test@example.com",
+ "password": password,
+ "role": UserRole.USER
+ }
+
+ with pytest.raises(ValidationError) as exc_info:
+ UserCreate(**user_data)
+
+ assert "password" in str(exc_info.value).lower()
+
+ def test_i_cannot_create_user_without_digit(self):
+ """Test that passwords without digits are rejected."""
+ passwords_without_digit = [
+ "TestPassword!",
+ "MyPassword!",
+ "UpperLower!"
+ ]
+
+ for password in passwords_without_digit:
+ user_data = {
+ "username": "testuser",
+ "email": "test@example.com",
+ "password": password,
+ "role": UserRole.USER
+ }
+
+ with pytest.raises(ValidationError) as exc_info:
+ UserCreate(**user_data)
+
+ assert "password" in str(exc_info.value).lower()
+
+ def test_i_cannot_create_user_without_special_character(self):
+ """Test that passwords without special characters are rejected."""
+ passwords_without_special = [
+ "TestPass123",
+ "MyPassword1",
+ "UpperLower1"
+ ]
+
+ for password in passwords_without_special:
+ user_data = {
+ "username": "testuser",
+ "email": "test@example.com",
+ "password": password,
+ "role": UserRole.USER
+ }
+
+ with pytest.raises(ValidationError) as exc_info:
+ UserCreate(**user_data)
+
+ assert "password" in str(exc_info.value).lower()
+
+ def test_i_cannot_create_user_with_empty_username(self):
+ """Test that empty username is rejected."""
+ user_data = {
+ "username": "",
+ "email": "test@example.com",
+ "password": "TestPass123!",
+ "role": UserRole.USER
+ }
+
+ with pytest.raises(ValidationError) as exc_info:
+ UserCreate(**user_data)
+
+ assert "username" in str(exc_info.value).lower()
+
+ def test_i_cannot_create_user_with_whitespace_username(self):
+ """Test that username with only whitespace is rejected."""
+ whitespace_usernames = [" ", "\t", "\n", " \t\n "]
+
+ for username in whitespace_usernames:
+ user_data = {
+ "username": username,
+ "email": "test@example.com",
+ "password": "TestPass123!",
+ "role": UserRole.USER
+ }
+
+ with pytest.raises(ValidationError) as exc_info:
+ UserCreate(**user_data)
+
+ assert "username" in str(exc_info.value).lower()
+
+
+class TestUserUpdateModel:
+ """Tests for UserUpdate Pydantic model validation."""
+
+ def test_i_can_create_user_update_model(self):
+ """Test creation of valid UserUpdate model with optional fields."""
+ update_data = {
+ "email": "newemail@example.com",
+ "role": UserRole.ADMIN
+ }
+
+ user_update = UserUpdate(**update_data)
+
+ assert user_update.email == "newemail@example.com"
+ assert user_update.role == UserRole.ADMIN
+ assert user_update.username is None
+ assert user_update.password is None
+
+ def test_i_can_create_empty_user_update_model(self):
+ """Test creation of UserUpdate model with no fields (all optional)."""
+ user_update = UserUpdate()
+
+ assert user_update.username is None
+ assert user_update.email is None
+ assert user_update.password is None
+ assert user_update.role is None
+
+ def test_i_can_update_password_with_valid_format(self):
+ """Test that valid password can be used in update."""
+ update_data = {
+ "password": "NewPass123!"
+ }
+
+ user_update = UserUpdate(**update_data)
+
+ assert user_update.password == "NewPass123!"
+
+ def test_i_cannot_update_with_invalid_password(self):
+ """Test that invalid password format is rejected in update."""
+ update_data = {
+ "password": "weak"
+ }
+
+ with pytest.raises(ValidationError) as exc_info:
+ UserUpdate(**update_data)
+
+ assert "password" in str(exc_info.value).lower()
+
+
+class TestUserInDBModel:
+ """Tests for UserInDB Pydantic model (database representation)."""
+
+ def test_i_can_create_user_in_db_model(self):
+ """Test creation of valid UserInDB model with all fields."""
+ user_id = ObjectId()
+ created_at = datetime.utcnow()
+ updated_at = datetime.utcnow()
+
+ user_data = {
+ "id": user_id,
+ "username": "testuser",
+ "email": "test@example.com",
+ "password_hash": "$2b$12$hashedpassword",
+ "role": UserRole.USER,
+ "is_active": True,
+ "created_at": created_at,
+ "updated_at": updated_at
+ }
+
+ user = UserInDB(**user_data)
+
+ assert user.id == user_id
+ assert user.username == "testuser"
+ assert user.email == "test@example.com"
+ assert user.password_hash == "$2b$12$hashedpassword"
+ assert user.role == UserRole.USER
+ assert user.is_active is True
+ assert user.created_at == created_at
+ assert user.updated_at == updated_at
+
+ def test_i_can_create_inactive_user(self):
+ """Test creation of inactive user."""
+ user_data = {
+ "id": ObjectId(),
+ "username": "testuser",
+ "email": "test@example.com",
+ "password_hash": "$2b$12$hashedpassword",
+ "role": UserRole.USER,
+ "is_active": False,
+ "created_at": datetime.utcnow(),
+ "updated_at": datetime.utcnow()
+ }
+
+ user = UserInDB(**user_data)
+
+ assert user.is_active is False
+
+
+class TestUserResponseModel:
+ """Tests for UserResponse Pydantic model (API response)."""
+
+ def test_i_can_create_user_response_model(self):
+ """Test creation of valid UserResponse model without password."""
+ user_id = ObjectId()
+ created_at = datetime.utcnow()
+ updated_at = datetime.utcnow()
+
+ user_data = {
+ "id": user_id,
+ "username": "testuser",
+ "email": "test@example.com",
+ "role": UserRole.USER,
+ "is_active": True,
+ "created_at": created_at,
+ "updated_at": updated_at
+ }
+
+ user = UserResponse(**user_data)
+
+ assert user.id == user_id
+ assert user.username == "testuser"
+ assert user.email == "test@example.com"
+ assert user.role == UserRole.USER
+ assert user.is_active is True
+ assert user.created_at == created_at
+ assert user.updated_at == updated_at
+ # Verify password_hash is not included
+ assert not hasattr(user, 'password_hash')
+
+ def test_user_response_excludes_password_hash(self):
+ """Test that UserResponse model does not expose password_hash."""
+ # This test verifies the model structure doesn't include password_hash
+ response_fields = UserResponse.__fields__.keys()
+
+ assert 'password_hash' not in response_fields
+ assert 'username' in response_fields
+ assert 'email' in response_fields
+ assert 'role' in response_fields
+ assert 'is_active' in response_fields
+
+ def test_i_can_convert_user_in_db_to_response(self):
+ """Test conversion from UserInDB to UserResponse model."""
+ user_id = ObjectId()
+ created_at = datetime.utcnow()
+ updated_at = datetime.utcnow()
+
+ user_in_db = UserInDB(
+ id=user_id,
+ username="testuser",
+ email="test@example.com",
+ password_hash="$2b$12$hashedpassword",
+ role=UserRole.USER,
+ is_active=True,
+ created_at=created_at,
+ updated_at=updated_at
+ )
+
+ # Convert to response model (excluding password_hash)
+ user_response = UserResponse(
+ id=user_in_db.id,
+ username=user_in_db.username,
+ email=user_in_db.email,
+ role=user_in_db.role,
+ is_active=user_in_db.is_active,
+ created_at=user_in_db.created_at,
+ updated_at=user_in_db.updated_at
+ )
+
+ assert user_response.id == user_in_db.id
+ assert user_response.username == user_in_db.username
+ assert user_response.email == user_in_db.email
+ assert user_response.role == user_in_db.role
+ assert not hasattr(user_response, 'password_hash')
\ No newline at end of file
diff --git a/tests/test_user_repository.py b/tests/test_user_repository.py
new file mode 100644
index 0000000..a2a0279
--- /dev/null
+++ b/tests/test_user_repository.py
@@ -0,0 +1,385 @@
+"""
+Unit tests for user repository module.
+
+Tests all CRUD operations for users with MongoDB mocking
+to ensure proper database interactions without requiring
+actual MongoDB instance during tests.
+"""
+
+import pytest
+from unittest.mock import Mock, MagicMock
+from datetime import datetime
+from bson import ObjectId
+from pymongo.errors import DuplicateKeyError
+
+from app.database.repositories.user_repository import UserRepository
+from app.models.user import UserCreate, UserUpdate, UserInDB, UserRole
+
+
+@pytest.fixture
+def mock_database():
+ """Create mock database with users collection."""
+ db = Mock()
+ collection = Mock()
+ db.users = collection
+ return db
+
+
+@pytest.fixture
+def user_repository(mock_database):
+ """Create UserRepository instance with mocked database."""
+ return UserRepository(mock_database)
+
+
+@pytest.fixture
+def sample_user_create():
+ """Create sample UserCreate object for testing."""
+ return UserCreate(
+ username="testuser",
+ email="test@example.com",
+ hashed_password="hashed_password_123",
+ role=UserRole.USER,
+ is_active=True
+ )
+
+
+@pytest.fixture
+def sample_user_update():
+ """Create sample UserUpdate object for testing."""
+ return UserUpdate(
+ email="updated@example.com",
+ role=UserRole.ADMIN,
+ is_active=False
+ )
+
+
+def test_i_can_create_user(user_repository, mock_database, sample_user_create):
+ """Test successful user creation."""
+ # Mock successful insertion
+ mock_result = Mock()
+ mock_result.inserted_id = ObjectId()
+ mock_database.users.insert_one.return_value = mock_result
+
+ result = user_repository.create_user(sample_user_create)
+
+ assert isinstance(result, UserInDB)
+ assert result.username == sample_user_create.username
+ assert result.email == sample_user_create.email
+ assert result.hashed_password == sample_user_create.hashed_password
+ assert result.role == sample_user_create.role
+ assert result.is_active == sample_user_create.is_active
+ assert result.id is not None
+ assert isinstance(result.created_at, datetime)
+ assert isinstance(result.updated_at, datetime)
+
+ # Verify insert_one was called with correct data
+ mock_database.users.insert_one.assert_called_once()
+ call_args = mock_database.users.insert_one.call_args[0][0]
+ assert call_args["username"] == sample_user_create.username
+ assert call_args["email"] == sample_user_create.email
+
+
+def test_i_cannot_create_duplicate_username(user_repository, mock_database, sample_user_create):
+ """Test that creating user with duplicate username raises DuplicateKeyError."""
+ # Mock DuplicateKeyError from MongoDB
+ mock_database.users.insert_one.side_effect = DuplicateKeyError("duplicate key error")
+
+ with pytest.raises(DuplicateKeyError, match="User with username 'testuser' already exists"):
+ user_repository.create_user(sample_user_create)
+
+
+def test_i_can_find_user_by_username(user_repository, mock_database):
+ """Test finding user by username."""
+ # Mock user document from database
+ user_doc = {
+ "_id": ObjectId(),
+ "username": "testuser",
+ "email": "test@example.com",
+ "hashed_password": "hashed_password_123",
+ "role": "user",
+ "is_active": True,
+ "created_at": datetime.utcnow(),
+ "updated_at": datetime.utcnow()
+ }
+ mock_database.users.find_one.return_value = user_doc
+
+ result = user_repository.find_user_by_username("testuser")
+
+ assert isinstance(result, UserInDB)
+ assert result.username == "testuser"
+ assert result.email == "test@example.com"
+
+ mock_database.users.find_one.assert_called_once_with({"username": "testuser"})
+
+
+def test_i_cannot_find_nonexistent_user_by_username(user_repository, mock_database):
+ """Test finding nonexistent user by username returns None."""
+ mock_database.users.find_one.return_value = None
+
+ result = user_repository.find_user_by_username("nonexistent")
+
+ assert result is None
+ mock_database.users.find_one.assert_called_once_with({"username": "nonexistent"})
+
+
+def test_i_can_find_user_by_id(user_repository, mock_database):
+ """Test finding user by ID."""
+ user_id = ObjectId()
+ user_doc = {
+ "_id": user_id,
+ "username": "testuser",
+ "email": "test@example.com",
+ "hashed_password": "hashed_password_123",
+ "role": "user",
+ "is_active": True,
+ "created_at": datetime.utcnow(),
+ "updated_at": datetime.utcnow()
+ }
+ mock_database.users.find_one.return_value = user_doc
+
+ result = user_repository.find_user_by_id(str(user_id))
+
+ assert isinstance(result, UserInDB)
+ assert result.id == user_id
+ assert result.username == "testuser"
+
+ mock_database.users.find_one.assert_called_once_with({"_id": user_id})
+
+
+def test_i_cannot_find_user_with_invalid_id(user_repository, mock_database):
+ """Test finding user with invalid ObjectId returns None."""
+ result = user_repository.find_user_by_id("invalid_id")
+
+ assert result is None
+ # find_one should not be called with invalid ID
+ mock_database.users.find_one.assert_not_called()
+
+
+def test_i_cannot_find_nonexistent_user_by_id(user_repository, mock_database):
+ """Test finding nonexistent user by ID returns None."""
+ user_id = ObjectId()
+ mock_database.users.find_one.return_value = None
+
+ result = user_repository.find_user_by_id(str(user_id))
+
+ assert result is None
+ mock_database.users.find_one.assert_called_once_with({"_id": user_id})
+
+
+def test_i_can_find_user_by_email(user_repository, mock_database):
+ """Test finding user by email address."""
+ user_doc = {
+ "_id": ObjectId(),
+ "username": "testuser",
+ "email": "test@example.com",
+ "hashed_password": "hashed_password_123",
+ "role": "user",
+ "is_active": True,
+ "created_at": datetime.utcnow(),
+ "updated_at": datetime.utcnow()
+ }
+ mock_database.users.find_one.return_value = user_doc
+
+ result = user_repository.find_user_by_email("test@example.com")
+
+ assert isinstance(result, UserInDB)
+ assert result.email == "test@example.com"
+
+ mock_database.users.find_one.assert_called_once_with({"email": "test@example.com"})
+
+
+def test_i_can_update_user(user_repository, mock_database, sample_user_update):
+ """Test updating user information."""
+ user_id = ObjectId()
+
+ # Mock successful update
+ mock_update_result = Mock()
+ mock_update_result.matched_count = 1
+ mock_database.users.update_one.return_value = mock_update_result
+
+ # Mock find_one for returning updated user
+ updated_user_doc = {
+ "_id": user_id,
+ "username": "testuser",
+ "email": "updated@example.com",
+ "hashed_password": "hashed_password_123",
+ "role": "admin",
+ "is_active": False,
+ "created_at": datetime.utcnow(),
+ "updated_at": datetime.utcnow()
+ }
+ mock_database.users.find_one.return_value = updated_user_doc
+
+ result = user_repository.update_user(str(user_id), sample_user_update)
+
+ assert isinstance(result, UserInDB)
+ assert result.email == "updated@example.com"
+ assert result.role == UserRole.ADMIN
+ assert result.is_active is False
+
+ # Verify update_one was called with correct data
+ mock_database.users.update_one.assert_called_once()
+ call_args = mock_database.users.update_one.call_args
+ assert call_args[0][0] == {"_id": user_id} # Filter
+ update_data = call_args[0][1]["$set"] # Update data
+ assert update_data["email"] == "updated@example.com"
+ assert update_data["role"] == UserRole.ADMIN
+ assert update_data["is_active"] is False
+ assert "updated_at" in update_data
+
+
+def test_i_cannot_update_nonexistent_user(user_repository, mock_database, sample_user_update):
+ """Test updating nonexistent user returns None."""
+ user_id = ObjectId()
+
+ # Mock no match found
+ mock_update_result = Mock()
+ mock_update_result.matched_count = 0
+ mock_database.users.update_one.return_value = mock_update_result
+
+ result = user_repository.update_user(str(user_id), sample_user_update)
+
+ assert result is None
+
+
+def test_i_cannot_update_user_with_invalid_id(user_repository, mock_database, sample_user_update):
+ """Test updating user with invalid ID returns None."""
+ result = user_repository.update_user("invalid_id", sample_user_update)
+
+ assert result is None
+ # update_one should not be called with invalid ID
+ mock_database.users.update_one.assert_not_called()
+
+
+def test_i_can_delete_user(user_repository, mock_database):
+ """Test successful user deletion."""
+ user_id = ObjectId()
+
+ # Mock successful deletion
+ mock_delete_result = Mock()
+ mock_delete_result.deleted_count = 1
+ mock_database.users.delete_one.return_value = mock_delete_result
+
+ result = user_repository.delete_user(str(user_id))
+
+ assert result is True
+ mock_database.users.delete_one.assert_called_once_with({"_id": user_id})
+
+
+def test_i_cannot_delete_nonexistent_user(user_repository, mock_database):
+ """Test deleting nonexistent user returns False."""
+ user_id = ObjectId()
+
+ # Mock no deletion occurred
+ mock_delete_result = Mock()
+ mock_delete_result.deleted_count = 0
+ mock_database.users.delete_one.return_value = mock_delete_result
+
+ result = user_repository.delete_user(str(user_id))
+
+ assert result is False
+
+
+def test_i_cannot_delete_user_with_invalid_id(user_repository, mock_database):
+ """Test deleting user with invalid ID returns False."""
+ result = user_repository.delete_user("invalid_id")
+
+ assert result is False
+ # delete_one should not be called with invalid ID
+ mock_database.users.delete_one.assert_not_called()
+
+
+def test_i_can_list_users(user_repository, mock_database):
+ """Test listing users with pagination."""
+ # Mock cursor with user documents
+ user_docs = [
+ {
+ "_id": ObjectId(),
+ "username": "user1",
+ "email": "user1@example.com",
+ "hashed_password": "hash1",
+ "role": "user",
+ "is_active": True,
+ "created_at": datetime.utcnow(),
+ "updated_at": datetime.utcnow()
+ },
+ {
+ "_id": ObjectId(),
+ "username": "user2",
+ "email": "user2@example.com",
+ "hashed_password": "hash2",
+ "role": "admin",
+ "is_active": False,
+ "created_at": datetime.utcnow(),
+ "updated_at": datetime.utcnow()
+ }
+ ]
+
+ mock_cursor = Mock()
+ mock_cursor.__iter__.return_value = iter(user_docs)
+ mock_cursor.skip.return_value = mock_cursor
+ mock_cursor.limit.return_value = mock_cursor
+ mock_database.users.find.return_value = mock_cursor
+
+ result = user_repository.list_users(skip=10, limit=50)
+
+ assert len(result) == 2
+ assert all(isinstance(user, UserInDB) for user in result)
+ assert result[0].username == "user1"
+ assert result[1].username == "user2"
+
+ mock_database.users.find.assert_called_once()
+ mock_cursor.skip.assert_called_once_with(10)
+ mock_cursor.limit.assert_called_once_with(50)
+
+
+def test_i_can_count_users(user_repository, mock_database):
+ """Test counting total users."""
+ mock_database.users.count_documents.return_value = 42
+
+ result = user_repository.count_users()
+
+ assert result == 42
+ mock_database.users.count_documents.assert_called_once_with({})
+
+
+def test_i_can_check_user_exists(user_repository, mock_database):
+ """Test checking if user exists by username."""
+ mock_database.users.count_documents.return_value = 1
+
+ result = user_repository.user_exists("testuser")
+
+ assert result is True
+ mock_database.users.count_documents.assert_called_once_with({"username": "testuser"})
+
+
+def test_i_can_check_user_does_not_exist(user_repository, mock_database):
+ """Test checking if user does not exist by username."""
+ mock_database.users.count_documents.return_value = 0
+
+ result = user_repository.user_exists("nonexistent")
+
+ assert result is False
+ mock_database.users.count_documents.assert_called_once_with({"username": "nonexistent"})
+
+
+def test_i_can_create_indexes_on_initialization(mock_database):
+ """Test that indexes are created when repository is initialized."""
+ # Mock create_index to not raise exception
+ mock_database.users.create_index.return_value = None
+
+ repository = UserRepository(mock_database)
+
+ mock_database.users.create_index.assert_called_once_with("username", unique=True)
+
+
+def test_i_can_handle_index_creation_error(mock_database):
+ """Test that index creation errors are handled gracefully."""
+ # Mock create_index to raise exception (index already exists)
+ mock_database.users.create_index.side_effect = Exception("Index already exists")
+
+ # Should not raise exception
+ repository = UserRepository(mock_database)
+
+ assert repository is not None
+ mock_database.users.create_index.assert_called_once_with("username", unique=True)
\ No newline at end of file