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 logo + + + React logo + +
+

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