From b2330dee22bdb186fce61fb1d802f66f6a4735c5 Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 14 Jun 2025 13:36:31 -0400 Subject: [PATCH] Add chat page and chat server --- .gitignore | 8 +- docs/setup-chat.md | 23 + .../db-convert/mongo_to_postgres_converter.py | 881 ++++++++++ .../chat/db-convert/reset_database.sql | 41 + .../chat/db-convert/test_converter.py | 54 + inventory-server/chat/package-lock.json | 1447 +++++++++++++++++ inventory-server/chat/package.json | 20 + inventory-server/chat/routes.js | 87 + inventory-server/chat/server.js | 83 + inventory/src/App.tsx | 6 + inventory/src/components/chat/ChatTest.tsx | 177 ++ .../src/components/layout/AppSidebar.tsx | 7 + inventory/src/config.ts | 3 +- inventory/src/pages/Chat.tsx | 167 ++ inventory/tsconfig.node.json | 3 +- inventory/vite.config.js | 189 --- inventory/vite.config.ts | 35 + 17 files changed, 3039 insertions(+), 192 deletions(-) create mode 100644 docs/setup-chat.md create mode 100644 inventory-server/chat/db-convert/mongo_to_postgres_converter.py create mode 100644 inventory-server/chat/db-convert/reset_database.sql create mode 100644 inventory-server/chat/db-convert/test_converter.py create mode 100644 inventory-server/chat/package-lock.json create mode 100644 inventory-server/chat/package.json create mode 100644 inventory-server/chat/routes.js create mode 100644 inventory-server/chat/server.js create mode 100644 inventory/src/components/chat/ChatTest.tsx create mode 100644 inventory/src/pages/Chat.tsx delete mode 100644 inventory/vite.config.js diff --git a/.gitignore b/.gitignore index e7f8454..c90e69b 100644 --- a/.gitignore +++ b/.gitignore @@ -67,4 +67,10 @@ inventory-server/scripts/.fuse_hidden00000fa20000000a .VSCodeCounter/ .VSCodeCounter/* -.VSCodeCounter/**/* \ No newline at end of file +.VSCodeCounter/**/* + +*/chat/db-convert/db/* +*/chat/db-convert/mongo_converter_env/* + +# Ignore compiled Vite config to avoid duplication +vite.config.js \ No newline at end of file diff --git a/docs/setup-chat.md b/docs/setup-chat.md new file mode 100644 index 0000000..00804c0 --- /dev/null +++ b/docs/setup-chat.md @@ -0,0 +1,23 @@ +This portion of the application is going to be a read only chat archive. It will pull data from a rocketchat export converted to postgresql. This is a separate database than the rest of the inventory application uses, but it will still use users and permissions from the inventory database. Both databases are on the same postgres instance. + +For now, let's add a select to the top of the page that allows me to "view as" any of the users in the rocketchat database. We'll connect this to the authorization in the main application later. + +The db connection info is stored in the .env file in the inventory-server root. It contains these variables +DB_HOST=localhost +DB_USER=rocketchat_user +DB_PASSWORD=password +DB_NAME=rocketchat_converted +DB_PORT=5432 + +Not all of the information in this database is relevant as it's a direct export from another app with more features. You can use the query tool to examine the structure and data available. + +Server-side files should use similar conventions and the same technologies as the inventory-server (inventor-server root) and auth-server (inventory-server/auth). I will provide my current pm2 ecosystem file upon request for you to add the configuration for the new "chat-server". I use Caddy on the server and can provide my caddyfile to assist with configuring the api routes. All configuration and routes for the chat-server should go in the inventory-server/chat folder or subfolders you create. + +The folder you see as inventory-server is actually a direct mount of the /var/www/html/inventory folder on the server. You can read and write files from there like usual, but any terminal commands for the server I will have to run myself. + +The "Chat" page should be added to the main application sidebar and a similar page to the others should be created in inventory/src/pages. All other frontend pages should go in inventory/src/components/chat. + +The application uses shadcn components and those should be used for all ui elements where possible (located in inventory/src/components/ui). The UI should match existing pages and components. + + + diff --git a/inventory-server/chat/db-convert/mongo_to_postgres_converter.py b/inventory-server/chat/db-convert/mongo_to_postgres_converter.py new file mode 100644 index 0000000..3641d61 --- /dev/null +++ b/inventory-server/chat/db-convert/mongo_to_postgres_converter.py @@ -0,0 +1,881 @@ +#!/usr/bin/env python3 +""" +MongoDB to PostgreSQL Converter for Rocket.Chat +Converts MongoDB BSON export files to PostgreSQL database + +Usage: + python3 mongo_to_postgres_converter.py \ + --mongo-path db/database/62df06d44234d20001289144 \ + --pg-database rocketchat_converted \ + --pg-user rocketchat_user \ + --pg-password your_password \ + --debug +""" + +import json +import os +import re +import subprocess +import sys +import struct +from datetime import datetime +from pathlib import Path +from typing import Dict, Any, List, Optional +import argparse +import traceback + +# Auto-install dependencies if needed +try: + import bson + import psycopg2 +except ImportError: + print("Installing required packages...") + subprocess.check_call([sys.executable, "-m", "pip", "install", "pymongo", "psycopg2-binary"]) + import bson + import psycopg2 + +class MongoToPostgresConverter: + def __init__(self, mongo_db_path: str, postgres_config: Dict[str, str], debug_mode: bool = False, debug_collections: List[str] = None): + self.mongo_db_path = Path(mongo_db_path) + self.postgres_config = postgres_config + self.debug_mode = debug_mode + self.debug_collections = debug_collections or [] + self.collections = {} + self.schema_info = {} + self.error_log = {} + + def log_debug(self, message: str, collection: str = None): + """Log debug messages if debug mode is enabled and collection is in debug list""" + if self.debug_mode and (not self.debug_collections or collection in self.debug_collections): + print(f"DEBUG: {message}") + + def log_error(self, collection: str, error_type: str, details: str): + """Log detailed error information""" + if collection not in self.error_log: + self.error_log[collection] = [] + self.error_log[collection].append({ + 'type': error_type, + 'details': details, + 'timestamp': datetime.now().isoformat() + }) + + def sample_documents(self, collection_name: str, max_samples: int = 3) -> List[Dict]: + """Sample documents from a collection for debugging""" + if not self.debug_mode or (self.debug_collections and collection_name not in self.debug_collections): + return [] + + print(f"\n๐Ÿ” Sampling documents from {collection_name}:") + + bson_file = self.collections[collection_name]['bson_file'] + if bson_file.stat().st_size == 0: + print(" Collection is empty") + return [] + + samples = [] + + try: + with open(bson_file, 'rb') as f: + sample_count = 0 + while sample_count < max_samples: + try: + doc_size = int.from_bytes(f.read(4), byteorder='little') + if doc_size <= 0: + break + f.seek(-4, 1) + doc_bytes = f.read(doc_size) + if len(doc_bytes) != doc_size: + break + + doc = bson.decode(doc_bytes) + samples.append(doc) + sample_count += 1 + + print(f" Sample {sample_count} - Keys: {list(doc.keys())}") + # Show a few key fields with their types and truncated values + for key, value in list(doc.items())[:3]: + value_preview = str(value)[:50] + "..." if len(str(value)) > 50 else str(value) + print(f" {key}: {type(value).__name__} = {value_preview}") + if len(doc) > 3: + print(f" ... and {len(doc) - 3} more fields") + print() + + except (bson.InvalidBSON, struct.error, OSError) as e: + self.log_error(collection_name, 'document_parsing', str(e)) + break + + except Exception as e: + self.log_error(collection_name, 'file_reading', str(e)) + print(f" Error reading collection: {e}") + + return samples + + def discover_collections(self): + """Discover all BSON files and their metadata""" + print("Discovering MongoDB collections...") + + for bson_file in self.mongo_db_path.glob("*.bson"): + collection_name = bson_file.stem + metadata_file = bson_file.with_suffix(".metadata.json") + + # Read metadata if available + metadata = {} + if metadata_file.exists(): + try: + with open(metadata_file, 'r', encoding='utf-8') as f: + metadata = json.load(f) + except (UnicodeDecodeError, json.JSONDecodeError) as e: + print(f"Warning: Could not read metadata for {collection_name}: {e}") + metadata = {} + + # Get file size and document count estimate + file_size = bson_file.stat().st_size + doc_count = self._estimate_document_count(bson_file) + + self.collections[collection_name] = { + 'bson_file': bson_file, + 'metadata': metadata, + 'file_size': file_size, + 'estimated_docs': doc_count + } + + print(f"Found {len(self.collections)} collections") + for name, info in self.collections.items(): + print(f" - {name}: {info['file_size']/1024/1024:.1f}MB (~{info['estimated_docs']} docs)") + + def _estimate_document_count(self, bson_file: Path) -> int: + """Estimate document count by reading first few documents""" + if bson_file.stat().st_size == 0: + return 0 + + try: + with open(bson_file, 'rb') as f: + docs_sampled = 0 + bytes_sampled = 0 + max_sample_size = min(1024 * 1024, bson_file.stat().st_size) # 1MB or file size + + while bytes_sampled < max_sample_size: + try: + doc_size = int.from_bytes(f.read(4), byteorder='little') + if doc_size <= 0 or doc_size > 16 * 1024 * 1024: # MongoDB doc size limit + break + f.seek(-4, 1) # Go back + doc_bytes = f.read(doc_size) + if len(doc_bytes) != doc_size: + break + bson.decode(doc_bytes) # Validate it's a valid BSON document + docs_sampled += 1 + bytes_sampled += doc_size + except (bson.InvalidBSON, struct.error, OSError): + break + + if docs_sampled > 0 and bytes_sampled > 0: + avg_doc_size = bytes_sampled / docs_sampled + return int(bson_file.stat().st_size / avg_doc_size) + + except Exception: + pass + + return 0 + + def analyze_schema(self, collection_name: str, sample_size: int = 100) -> Dict[str, Any]: + """Analyze collection schema by sampling documents""" + print(f"Analyzing schema for {collection_name}...") + + bson_file = self.collections[collection_name]['bson_file'] + if bson_file.stat().st_size == 0: + return {} + + schema = {} + docs_analyzed = 0 + + try: + with open(bson_file, 'rb') as f: + while docs_analyzed < sample_size: + try: + doc_size = int.from_bytes(f.read(4), byteorder='little') + if doc_size <= 0: + break + f.seek(-4, 1) + doc_bytes = f.read(doc_size) + if len(doc_bytes) != doc_size: + break + + doc = bson.decode(doc_bytes) + self._analyze_document_schema(doc, schema) + docs_analyzed += 1 + + except (bson.InvalidBSON, struct.error, OSError): + break + + except Exception as e: + print(f"Error analyzing {collection_name}: {e}") + + self.schema_info[collection_name] = schema + return schema + + def _analyze_document_schema(self, doc: Dict[str, Any], schema: Dict[str, Any], prefix: str = ""): + """Recursively analyze document structure""" + for key, value in doc.items(): + full_key = f"{prefix}.{key}" if prefix else key + + if full_key not in schema: + schema[full_key] = { + 'types': set(), + 'null_count': 0, + 'total_count': 0, + 'is_array': False, + 'nested_schema': {} + } + + schema[full_key]['total_count'] += 1 + + if value is None: + schema[full_key]['null_count'] += 1 + schema[full_key]['types'].add('null') + elif isinstance(value, dict): + schema[full_key]['types'].add('object') + if 'nested_schema' not in schema[full_key]: + schema[full_key]['nested_schema'] = {} + self._analyze_document_schema(value, schema[full_key]['nested_schema']) + elif isinstance(value, list): + schema[full_key]['types'].add('array') + schema[full_key]['is_array'] = True + if value and isinstance(value[0], dict): + if 'array_item_schema' not in schema[full_key]: + schema[full_key]['array_item_schema'] = {} + for item in value[:5]: # Sample first 5 items + if isinstance(item, dict): + self._analyze_document_schema(item, schema[full_key]['array_item_schema']) + else: + schema[full_key]['types'].add(type(value).__name__) + + def generate_postgres_schema(self) -> Dict[str, str]: + """Generate PostgreSQL CREATE TABLE statements""" + print("Generating PostgreSQL schema...") + + table_definitions = {} + + for collection_name, schema in self.schema_info.items(): + if not schema: # Empty collection + continue + + table_name = self._sanitize_table_name(collection_name) + columns = [] + + # Always add an id column (PostgreSQL doesn't use _id like MongoDB) + columns.append("id SERIAL PRIMARY KEY") + + for field_name, field_info in schema.items(): + if field_name == '_id': + columns.append("mongo_id TEXT") # Always allow NULL for mongo_id + continue + + col_name = self._sanitize_column_name(field_name) + + # Handle conflicts with PostgreSQL auto-generated columns + if col_name in ['id', 'mongo_id', 'created_at', 'updated_at']: + col_name = f"field_{col_name}" + + col_type = self._determine_postgres_type(field_info) + + # Make all fields nullable by default to avoid constraint violations + columns.append(f"{col_name} {col_type}") + + # Add metadata columns + columns.extend([ + "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP", + "updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP" + ]) + + column_definitions = ',\n '.join(columns) + table_sql = f""" +CREATE TABLE IF NOT EXISTS {table_name} ( + {column_definitions} +); + +-- Create indexes based on MongoDB indexes +""" + + # Get list of actual columns that will exist in the table + existing_columns = set(['id', 'mongo_id', 'created_at', 'updated_at']) + for field_name in schema.keys(): + if field_name != '_id': + col_name = self._sanitize_column_name(field_name) + # Handle conflicts with PostgreSQL auto-generated columns + if col_name in ['id', 'mongo_id', 'created_at', 'updated_at']: + col_name = f"field_{col_name}" + existing_columns.add(col_name) + + # Add indexes from MongoDB metadata + metadata = self.collections[collection_name].get('metadata', {}) + indexes = metadata.get('indexes', []) + + for index in indexes: + if index['name'] != '_id_': # Skip the default _id index + # Sanitize index name - remove special characters + sanitized_index_name = re.sub(r'[^a-zA-Z0-9_]', '_', index['name']) + index_name = f"idx_{table_name}_{sanitized_index_name}" + index_keys = list(index['key'].keys()) + if index_keys: + sanitized_keys = [] + for key in index_keys: + if key != '_id': + sanitized_key = self._sanitize_column_name(key) + # Handle conflicts with PostgreSQL auto-generated columns + if sanitized_key in ['id', 'mongo_id', 'created_at', 'updated_at']: + sanitized_key = f"field_{sanitized_key}" + # Only add if the column actually exists in our table + if sanitized_key in existing_columns: + sanitized_keys.append(sanitized_key) + + if sanitized_keys: + table_sql += f"CREATE INDEX IF NOT EXISTS {index_name} ON {table_name} ({', '.join(sanitized_keys)});\n" + + table_definitions[collection_name] = table_sql + + return table_definitions + + def _sanitize_table_name(self, name: str) -> str: + """Convert MongoDB collection name to PostgreSQL table name""" + # Remove rocketchat_ prefix if present + if name.startswith('rocketchat_'): + name = name[11:] + + # Replace special characters with underscores + name = re.sub(r'[^a-zA-Z0-9_]', '_', name) + + # Ensure it starts with a letter + if name and name[0].isdigit(): + name = 'table_' + name + + return name.lower() + + def _sanitize_column_name(self, name: str) -> str: + """Convert MongoDB field name to PostgreSQL column name""" + # Handle nested field names (convert dots to underscores) + name = name.replace('.', '_') + + # Replace special characters with underscores + name = re.sub(r'[^a-zA-Z0-9_]', '_', name) + + # Ensure it starts with a letter or underscore + if name and name[0].isdigit(): + name = 'col_' + name + + # Handle PostgreSQL reserved words + reserved = { + 'user', 'order', 'group', 'table', 'index', 'key', 'value', 'date', 'time', 'timestamp', + 'default', 'select', 'from', 'where', 'insert', 'update', 'delete', 'create', 'drop', + 'alter', 'grant', 'revoke', 'commit', 'rollback', 'begin', 'end', 'case', 'when', + 'then', 'else', 'if', 'null', 'not', 'and', 'or', 'in', 'exists', 'between', + 'like', 'limit', 'offset', 'union', 'join', 'inner', 'outer', 'left', 'right', + 'full', 'cross', 'natural', 'on', 'using', 'distinct', 'all', 'any', 'some', + 'desc', 'asc', 'primary', 'foreign', 'references', 'constraint', 'unique', + 'check', 'cascade', 'restrict', 'action', 'match', 'partial', 'full' + } + if name.lower() in reserved: + name = name + '_col' + + return name.lower() + + def _determine_postgres_type(self, field_info: Dict[str, Any]) -> str: + """Determine PostgreSQL column type from MongoDB field analysis with improved logic""" + types = field_info['types'] + + # Convert set to list for easier checking + type_list = list(types) + + # If there's only one type (excluding null), use specific typing + non_null_types = [t for t in type_list if t != 'null'] + + if len(non_null_types) == 1: + single_type = non_null_types[0] + if single_type == 'bool': + return 'BOOLEAN' + elif single_type == 'int': + return 'INTEGER' + elif single_type == 'float': + return 'NUMERIC' + elif single_type == 'str': + return 'TEXT' + elif single_type == 'datetime': + return 'TIMESTAMP' + elif single_type == 'ObjectId': + return 'TEXT' + + # Handle mixed types more conservatively + if 'array' in types or field_info.get('is_array', False): + return 'JSONB' # Arrays always go to JSONB + elif 'object' in types: + return 'JSONB' # Objects always go to JSONB + elif len(non_null_types) > 1: + # Multiple non-null types - check for common combinations + if set(non_null_types) <= {'int', 'float'}: + return 'NUMERIC' # Can handle both int and float + elif set(non_null_types) <= {'bool', 'str'}: + return 'TEXT' # Convert everything to text + elif set(non_null_types) <= {'str', 'ObjectId'}: + return 'TEXT' # Both are string-like + else: + return 'JSONB' # Complex mixed types go to JSONB + elif 'ObjectId' in types: + return 'TEXT' + elif 'datetime' in types: + return 'TIMESTAMP' + elif 'bool' in types: + return 'BOOLEAN' + elif 'int' in types: + return 'INTEGER' + elif 'float' in types: + return 'NUMERIC' + elif 'str' in types: + return 'TEXT' + else: + return 'TEXT' # Default fallback + + def create_postgres_database(self, table_definitions: Dict[str, str]): + """Create PostgreSQL database and tables""" + print("Creating PostgreSQL database schema...") + + try: + # Connect to PostgreSQL + conn = psycopg2.connect(**self.postgres_config) + conn.autocommit = True + cursor = conn.cursor() + + # Create tables + for collection_name, table_sql in table_definitions.items(): + print(f"Creating table for {collection_name}...") + cursor.execute(table_sql) + + cursor.close() + conn.close() + print("Database schema created successfully!") + + except Exception as e: + print(f"Error creating database schema: {e}") + raise + + def convert_and_insert_data(self, batch_size: int = 1000): + """Convert BSON data and insert into PostgreSQL""" + print("Converting and inserting data...") + + try: + conn = psycopg2.connect(**self.postgres_config) + conn.autocommit = False + + for collection_name in self.collections: + print(f"Processing {collection_name}...") + self._convert_collection(conn, collection_name, batch_size) + + conn.close() + print("Data conversion completed successfully!") + + except Exception as e: + print(f"Error converting data: {e}") + raise + + def _convert_collection(self, conn, collection_name: str, batch_size: int): + """Convert a single collection""" + bson_file = self.collections[collection_name]['bson_file'] + + if bson_file.stat().st_size == 0: + print(f" Skipping empty collection {collection_name}") + return + + table_name = self._sanitize_table_name(collection_name) + cursor = conn.cursor() + + batch = [] + total_inserted = 0 + errors = 0 + + try: + with open(bson_file, 'rb') as f: + while True: + try: + doc_size = int.from_bytes(f.read(4), byteorder='little') + if doc_size <= 0: + break + f.seek(-4, 1) + doc_bytes = f.read(doc_size) + if len(doc_bytes) != doc_size: + break + + doc = bson.decode(doc_bytes) + batch.append(doc) + + if len(batch) >= batch_size: + inserted, batch_errors = self._insert_batch(cursor, table_name, batch, collection_name) + total_inserted += inserted + errors += batch_errors + batch = [] + conn.commit() + if total_inserted % 5000 == 0: # Less frequent progress updates + print(f" Inserted {total_inserted} documents...") + + except (bson.InvalidBSON, struct.error, OSError): + break + + # Insert remaining documents + if batch: + inserted, batch_errors = self._insert_batch(cursor, table_name, batch, collection_name) + total_inserted += inserted + errors += batch_errors + conn.commit() + + if errors > 0: + print(f" Completed {collection_name}: {total_inserted} documents inserted ({errors} errors)") + else: + print(f" Completed {collection_name}: {total_inserted} documents inserted") + + except Exception as e: + print(f" Error processing {collection_name}: {e}") + conn.rollback() + finally: + cursor.close() + + def _insert_batch(self, cursor, table_name: str, documents: List[Dict], collection_name: str): + """Insert a batch of documents with proper transaction handling""" + if not documents: + return 0, 0 + + # Get schema info for this collection + schema = self.schema_info.get(collection_name, {}) + + # Build column list + columns = ['mongo_id'] + for field_name in schema.keys(): + if field_name != '_id': + col_name = self._sanitize_column_name(field_name) + # Handle conflicts with PostgreSQL auto-generated columns + if col_name in ['id', 'mongo_id', 'created_at', 'updated_at']: + col_name = f"field_{col_name}" + columns.append(col_name) + + # Build INSERT statement + placeholders = ', '.join(['%s'] * len(columns)) + sql = f"INSERT INTO {table_name} ({', '.join(columns)}) VALUES ({placeholders})" + + self.log_debug(f"SQL: {sql}", collection_name) + + # Convert documents to tuples + rows = [] + errors = 0 + + for doc_idx, doc in enumerate(documents): + try: + row = [] + + # Add mongo_id + row.append(str(doc.get('_id', ''))) + + # Add other fields + for field_name in schema.keys(): + if field_name != '_id': + try: + value = self._get_nested_value(doc, field_name) + converted_value = self._convert_value_for_postgres(value, field_name, schema) + row.append(converted_value) + except Exception as e: + self.log_error(collection_name, 'field_conversion', + f"Field '{field_name}' in doc {doc_idx}: {str(e)}") + # Only show debug for collections we're focusing on + if collection_name in self.debug_collections: + print(f" โš ๏ธ Error converting field '{field_name}': {e}") + row.append(None) # Use NULL for problematic fields + + rows.append(tuple(row)) + + except Exception as e: + self.log_error(collection_name, 'document_conversion', f"Document {doc_idx}: {str(e)}") + errors += 1 + continue + + # Execute batch insert + if rows: + try: + cursor.executemany(sql, rows) + return len(rows), errors + except Exception as batch_error: + self.log_error(collection_name, 'batch_insert', str(batch_error)) + + # Only show detailed debugging for targeted collections + if collection_name in self.debug_collections: + print(f" ๐Ÿ”ด Batch insert failed for {collection_name}: {batch_error}") + print(" Trying individual inserts with rollback handling...") + + # Rollback the failed transaction + cursor.connection.rollback() + + # Try inserting one by one in individual transactions + success_count = 0 + for row_idx, row in enumerate(rows): + try: + cursor.execute(sql, row) + cursor.connection.commit() # Commit each successful insert + success_count += 1 + except Exception as row_error: + cursor.connection.rollback() # Rollback failed insert + self.log_error(collection_name, 'row_insert', f"Row {row_idx}: {str(row_error)}") + + # Show detailed error only for the first few failures and only for targeted collections + if collection_name in self.debug_collections and errors < 3: + print(f" Row {row_idx} failed: {row_error}") + print(f" Row data: {len(row)} values, expected {len(columns)} columns") + + errors += 1 + continue + return success_count, errors + + return 0, errors + + def _get_nested_value(self, doc: Dict, field_path: str): + """Get value from nested document using dot notation""" + keys = field_path.split('.') + value = doc + + for key in keys: + if isinstance(value, dict) and key in value: + value = value[key] + else: + return None + + return value + + def _convert_value_for_postgres(self, value, field_name: str = None, schema: Dict = None): + """Convert MongoDB value to PostgreSQL compatible value with schema-aware conversion""" + if value is None: + return None + + # Get the expected PostgreSQL type for this field if available + expected_type = None + if schema and field_name and field_name in schema: + field_info = schema[field_name] + expected_type = self._determine_postgres_type(field_info) + + # Handle conversion based on expected type + if expected_type == 'BOOLEAN': + if isinstance(value, bool): + return value + elif isinstance(value, str): + return value.lower() in ('true', '1', 'yes', 'on') + elif isinstance(value, (int, float)): + return bool(value) + else: + return None + elif expected_type == 'INTEGER': + if isinstance(value, int): + return value + elif isinstance(value, float): + return int(value) + elif isinstance(value, str) and value.isdigit(): + return int(value) + elif isinstance(value, bool): + return int(value) + else: + return None + elif expected_type == 'NUMERIC': + if isinstance(value, (int, float)): + return value + elif isinstance(value, str): + try: + return float(value) + except ValueError: + return None + elif isinstance(value, bool): + return float(value) + else: + return None + elif expected_type == 'TEXT': + if isinstance(value, str): + return value + elif value is not None: + str_value = str(value) + # Handle very long strings + if len(str_value) > 65535: + return str_value[:65535] + return str_value + else: + return None + elif expected_type == 'TIMESTAMP': + if hasattr(value, 'isoformat'): + return value.isoformat() + elif isinstance(value, str): + return value + else: + return str(value) if value is not None else None + elif expected_type == 'JSONB': + if isinstance(value, (dict, list)): + return json.dumps(value, default=self._json_serializer) + elif isinstance(value, str): + # Check if it's already valid JSON + try: + json.loads(value) + return value + except (json.JSONDecodeError, TypeError): + # Not valid JSON, wrap it + return json.dumps(value) + else: + return json.dumps(value, default=self._json_serializer) + + # Fallback to original logic if no expected type or type not recognized + if isinstance(value, bool): + return value + elif isinstance(value, (int, float)): + return value + elif isinstance(value, str): + return value + elif isinstance(value, (dict, list)): + return json.dumps(value, default=self._json_serializer) + elif hasattr(value, 'isoformat'): # datetime + return value.isoformat() + elif hasattr(value, '__str__'): + str_value = str(value) + if len(str_value) > 65535: + return str_value[:65535] + return str_value + else: + return str(value) + + def _json_serializer(self, obj): + """Custom JSON serializer for complex objects with better error handling""" + try: + if hasattr(obj, 'isoformat'): # datetime + return obj.isoformat() + elif hasattr(obj, '__str__'): + return str(obj) + else: + return None + except Exception as e: + self.log_debug(f"JSON serialization error: {e}") + return str(obj) + + def run_conversion(self, sample_size: int = 100, batch_size: int = 1000): + """Run the full conversion process with focused debugging""" + print("Starting MongoDB to PostgreSQL conversion...") + print("This will convert your Rocket.Chat database from MongoDB to PostgreSQL") + if self.debug_mode: + if self.debug_collections: + print(f"๐Ÿ› DEBUG MODE: Focusing on collections: {', '.join(self.debug_collections)}") + else: + print("๐Ÿ› DEBUG MODE: All collections") + print("=" * 70) + + # Step 1: Discover collections + self.discover_collections() + + # Step 2: Analyze schemas + print("\nAnalyzing collection schemas...") + for collection_name in self.collections: + self.analyze_schema(collection_name, sample_size) + + # Sample problematic collections if debugging + if self.debug_mode and self.debug_collections: + for coll in self.debug_collections: + if coll in self.collections: + self.sample_documents(coll, 2) + + # Step 3: Generate PostgreSQL schema + table_definitions = self.generate_postgres_schema() + + # Step 4: Create database schema + self.create_postgres_database(table_definitions) + + # Step 5: Convert and insert data + self.convert_and_insert_data(batch_size) + + # Step 6: Show error summary + self._print_error_summary() + + print("=" * 70) + print("โœ… Conversion completed!") + print(f" Database: {self.postgres_config['database']}") + print(f" Tables created: {len(table_definitions)}") + + def _print_error_summary(self): + """Print a focused summary of errors""" + if not self.error_log: + print("\nโœ… No errors encountered during conversion!") + return + + print("\nโš ๏ธ ERROR SUMMARY:") + print("=" * 50) + + # Sort by error count descending + sorted_collections = sorted(self.error_log.items(), + key=lambda x: len(x[1]), reverse=True) + + for collection, errors in sorted_collections: + error_types = {} + for error in errors: + error_type = error['type'] + if error_type not in error_types: + error_types[error_type] = [] + error_types[error_type].append(error['details']) + + print(f"\n๐Ÿ”ด {collection} ({len(errors)} total errors):") + for error_type, details_list in error_types.items(): + print(f" {error_type}: {len(details_list)} errors") + + # Show sample errors for critical collections + if collection in ['rocketchat_settings', 'rocketchat_room'] and len(details_list) > 0: + print(f" Sample: {details_list[0][:100]}...") + +def main(): + parser = argparse.ArgumentParser( + description='Convert MongoDB BSON export to PostgreSQL', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Basic usage + python3 mongo_to_postgres_converter.py \\ + --mongo-path db/database/62df06d44234d20001289144 \\ + --pg-database rocketchat_converted \\ + --pg-user rocketchat_user \\ + --pg-password mypassword + + # Debug specific failing collections + python3 mongo_to_postgres_converter.py \\ + --mongo-path db/database/62df06d44234d20001289144 \\ + --pg-database rocketchat_converted \\ + --pg-user rocketchat_user \\ + --pg-password mypassword \\ + --debug-collections rocketchat_settings rocketchat_room + +Before running this script: +1. Run: sudo -u postgres psql -f reset_database.sql +2. Update the password in reset_database.sql + """ + ) + + parser.add_argument('--mongo-path', required=True, help='Path to MongoDB export directory') + parser.add_argument('--pg-host', default='localhost', help='PostgreSQL host (default: localhost)') + parser.add_argument('--pg-port', default='5432', help='PostgreSQL port (default: 5432)') + parser.add_argument('--pg-database', required=True, help='PostgreSQL database name') + parser.add_argument('--pg-user', required=True, help='PostgreSQL username') + parser.add_argument('--pg-password', required=True, help='PostgreSQL password') + parser.add_argument('--sample-size', type=int, default=100, help='Number of documents to sample for schema analysis (default: 100)') + parser.add_argument('--batch-size', type=int, default=1000, help='Batch size for data insertion (default: 1000)') + parser.add_argument('--debug', action='store_true', help='Enable debug mode with detailed error logging') + parser.add_argument('--debug-collections', nargs='*', help='Specific collections to debug (e.g., rocketchat_settings rocketchat_room)') + + args = parser.parse_args() + + postgres_config = { + 'host': args.pg_host, + 'port': args.pg_port, + 'database': args.pg_database, + 'user': args.pg_user, + 'password': args.pg_password + } + + # Enable debug mode if debug collections are specified + debug_mode = args.debug or (args.debug_collections is not None) + + converter = MongoToPostgresConverter(args.mongo_path, postgres_config, debug_mode, args.debug_collections) + converter.run_conversion(args.sample_size, args.batch_size) + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/inventory-server/chat/db-convert/reset_database.sql b/inventory-server/chat/db-convert/reset_database.sql new file mode 100644 index 0000000..7a91e95 --- /dev/null +++ b/inventory-server/chat/db-convert/reset_database.sql @@ -0,0 +1,41 @@ +-- PostgreSQL Database Reset Script for Rocket.Chat Import +-- Run as: sudo -u postgres psql -f reset_database.sql + +-- Terminate all connections to the database (force disconnect users) +SELECT pg_terminate_backend(pid) +FROM pg_stat_activity +WHERE datname = 'rocketchat_converted' AND pid <> pg_backend_pid(); + +-- Drop the database if it exists +DROP DATABASE IF EXISTS rocketchat_converted; + +-- Create fresh database +CREATE DATABASE rocketchat_converted; + +-- Create user (if not exists) +DO $$ +BEGIN + IF NOT EXISTS (SELECT FROM pg_user WHERE usename = 'rocketchat_user') THEN + CREATE USER rocketchat_user WITH PASSWORD 'HKjLgt23gWuPXzEAn3rW'; + END IF; +END $$; + +-- Grant database privileges +GRANT CONNECT ON DATABASE rocketchat_converted TO rocketchat_user; +GRANT CREATE ON DATABASE rocketchat_converted TO rocketchat_user; + +-- Connect to the new database +\c rocketchat_converted; + +-- Grant schema privileges +GRANT CREATE ON SCHEMA public TO rocketchat_user; +GRANT USAGE ON SCHEMA public TO rocketchat_user; + +-- Grant privileges on all future tables and sequences +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO rocketchat_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO rocketchat_user; + +-- Display success message +\echo 'Database reset completed successfully!' +\echo 'You can now run the converter with:' +\echo 'python3 mongo_to_postgres_converter.py --mongo-path db/database/62df06d44234d20001289144 --pg-database rocketchat_converted --pg-user rocketchat_user --pg-password your_password' \ No newline at end of file diff --git a/inventory-server/chat/db-convert/test_converter.py b/inventory-server/chat/db-convert/test_converter.py new file mode 100644 index 0000000..071b5c9 --- /dev/null +++ b/inventory-server/chat/db-convert/test_converter.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +""" +Quick test script to verify the converter fixes work for problematic collections +""" + +from mongo_to_postgres_converter import MongoToPostgresConverter + +def test_problematic_collections(): + print("๐Ÿงช Testing converter fixes for problematic collections...") + + postgres_config = { + 'host': 'localhost', + 'port': '5432', + 'database': 'rocketchat_test', + 'user': 'rocketchat_user', + 'password': 'password123' + } + + converter = MongoToPostgresConverter( + 'db/database/62df06d44234d20001289144', + postgres_config, + debug_mode=True, + debug_collections=['rocketchat_settings', 'rocketchat_room'] + ) + + # Test just discovery and schema analysis + print("\n1. Testing collection discovery...") + converter.discover_collections() + + print("\n2. Testing schema analysis...") + if 'rocketchat_settings' in converter.collections: + settings_schema = converter.analyze_schema('rocketchat_settings', 10) + print(f"Settings schema fields: {len(settings_schema)}") + + # Check specific problematic fields + if 'packageValue' in settings_schema: + packagevalue_info = settings_schema['packageValue'] + pg_type = converter._determine_postgres_type(packagevalue_info) + print(f"packageValue types: {packagevalue_info['types']} -> PostgreSQL: {pg_type}") + + if 'rocketchat_room' in converter.collections: + room_schema = converter.analyze_schema('rocketchat_room', 10) + print(f"Room schema fields: {len(room_schema)}") + + # Check specific problematic fields + if 'sysMes' in room_schema: + sysmes_info = room_schema['sysMes'] + pg_type = converter._determine_postgres_type(sysmes_info) + print(f"sysMes types: {sysmes_info['types']} -> PostgreSQL: {pg_type}") + + print("\nโœ… Test completed - check the type mappings above!") + +if __name__ == '__main__': + test_problematic_collections() \ No newline at end of file diff --git a/inventory-server/chat/package-lock.json b/inventory-server/chat/package-lock.json new file mode 100644 index 0000000..c90e008 --- /dev/null +++ b/inventory-server/chat/package-lock.json @@ -0,0 +1,1447 @@ +{ + "name": "chat-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "chat-server", + "version": "1.0.0", + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^16.0.3", + "express": "^4.18.2", + "morgan": "^1.10.0", + "pg": "^8.11.0" + }, + "devDependencies": { + "nodemon": "^3.1.10" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "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/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "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/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "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/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/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/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.0.tgz", + "integrity": "sha512-7SKfdvP8CTNXjMUzfcVTaI+TDzBEeaUnVwiVGZQD1Hh33Kpev7liQba9uLd4CfN8r9mCVsD0JIpq03+Unpz+kg==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.9.0", + "pg-pool": "^3.10.0", + "pg-protocol": "^1.10.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.5" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.5.tgz", + "integrity": "sha512-OOX22Vt0vOSRrdoUPKJ8Wi2OpE/o/h9T8X1s4qSkCedbNah9ei2W2765be8iMVxQUsvgT7zIAT2eIa9fs5+vtg==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.0.tgz", + "integrity": "sha512-P2DEBKuvh5RClafLngkAuGe9OUlFV7ebu8w1kmaaOgPcpJd1RIFh7otETfI6hAR8YupOLFTY7nuvvIn7PLciUQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.0.tgz", + "integrity": "sha512-DzZ26On4sQ0KmqnO34muPcmKbhrjmyiO4lCCR0VwEd7MjmiKf5NTg/6+apUEu0NF7ESa37CGzFxH513CoUmWnA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.0.tgz", + "integrity": "sha512-IpdytjudNuLv8nhlHs/UrVBhU0e78J0oIS/0AVdTbWxSOkFUVdsHC/NrorO6nXsQNDTT1kzDSOMJubBQviX18Q==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/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==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/inventory-server/chat/package.json b/inventory-server/chat/package.json new file mode 100644 index 0000000..34db9a0 --- /dev/null +++ b/inventory-server/chat/package.json @@ -0,0 +1,20 @@ +{ + "name": "chat-server", + "version": "1.0.0", + "description": "Chat archive server for Rocket.Chat data", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js" + }, + "dependencies": { + "express": "^4.18.2", + "cors": "^2.8.5", + "pg": "^8.11.0", + "dotenv": "^16.0.3", + "morgan": "^1.10.0" + }, + "devDependencies": { + "nodemon": "^2.0.22" + } +} \ No newline at end of file diff --git a/inventory-server/chat/routes.js b/inventory-server/chat/routes.js new file mode 100644 index 0000000..bcf802e --- /dev/null +++ b/inventory-server/chat/routes.js @@ -0,0 +1,87 @@ +const express = require('express'); +const router = express.Router(); + +// Get all active users for the "view as" dropdown +router.get('/users', async (req, res) => { + try { + const result = await global.pool.query(` + SELECT id, username, name, type, active + FROM users + WHERE active = true AND type = 'user' + ORDER BY name ASC + `); + + res.json({ + status: 'success', + users: result.rows + }); + } catch (error) { + console.error('Error fetching users:', error); + res.status(500).json({ + status: 'error', + error: 'Failed to fetch users', + details: error.message + }); + } +}); + +// Get rooms for a specific user +router.get('/users/:userId/rooms', async (req, res) => { + const { userId } = req.params; + + try { + // Get rooms where the user is a member + const result = await global.pool.query(` + SELECT DISTINCT r.id, r.name, r.fname, r.t as type, r.msgs, r.lm as last_message_date + FROM room r, subscription s + WHERE s.rid = r.mongo_id + AND s.u->>'_id' = (SELECT mongo_id FROM users WHERE id = $1) + AND r.archived IS NOT TRUE + ORDER BY r.lm DESC NULLS LAST + LIMIT 50 + `, [userId]); + + res.json({ + status: 'success', + rooms: result.rows + }); + } catch (error) { + console.error('Error fetching user rooms:', error); + res.status(500).json({ + status: 'error', + error: 'Failed to fetch user rooms', + details: error.message + }); + } +}); + +// Get messages for a specific room +router.get('/rooms/:roomId/messages', async (req, res) => { + const { roomId } = req.params; + const { limit = 50, offset = 0 } = req.query; + + try { + const result = await global.pool.query(` + SELECT m.id, m.msg, m.ts, m.u, m._updatedat + FROM message m + JOIN room r ON m.rid = r.mongo_id + WHERE r.id = $1 + ORDER BY m.ts DESC + LIMIT $2 OFFSET $3 + `, [roomId, limit, offset]); + + res.json({ + status: 'success', + messages: result.rows + }); + } catch (error) { + console.error('Error fetching messages:', error); + res.status(500).json({ + status: 'error', + error: 'Failed to fetch messages', + details: error.message + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/inventory-server/chat/server.js b/inventory-server/chat/server.js new file mode 100644 index 0000000..7257cf5 --- /dev/null +++ b/inventory-server/chat/server.js @@ -0,0 +1,83 @@ +require('dotenv').config({ path: '../.env' }); +const express = require('express'); +const cors = require('cors'); +const { Pool } = require('pg'); +const morgan = require('morgan'); +const chatRoutes = require('./routes'); + +// Log startup configuration +console.log('Starting chat server with config:', { + host: process.env.CHAT_DB_HOST, + user: process.env.CHAT_DB_USER, + database: process.env.CHAT_DB_NAME || 'rocketchat_converted', + port: process.env.CHAT_DB_PORT, + chat_port: process.env.CHAT_PORT || 3014 +}); + +const app = express(); +const port = process.env.CHAT_PORT || 3014; + +// Database configuration for rocketchat_converted database +const pool = new Pool({ + host: process.env.CHAT_DB_HOST, + user: process.env.CHAT_DB_USER, + password: process.env.CHAT_DB_PASSWORD, + database: process.env.CHAT_DB_NAME || 'rocketchat_converted', + port: process.env.CHAT_DB_PORT, +}); + +// Make pool available globally +global.pool = pool; + +// Middleware +app.use(express.json()); +app.use(morgan('combined')); +app.use(cors({ + origin: ['http://localhost:5175', 'http://localhost:5174', 'https://inventory.kent.pw'], + credentials: true +})); + +// Test database connection endpoint +app.get('/test-db', async (req, res) => { + try { + const result = await pool.query('SELECT COUNT(*) as user_count FROM users WHERE active = true'); + const messageResult = await pool.query('SELECT COUNT(*) as message_count FROM message'); + const roomResult = await pool.query('SELECT COUNT(*) as room_count FROM room'); + + res.json({ + status: 'success', + database: 'rocketchat_converted', + stats: { + active_users: parseInt(result.rows[0].user_count), + total_messages: parseInt(messageResult.rows[0].message_count), + total_rooms: parseInt(roomResult.rows[0].room_count) + } + }); + } catch (error) { + console.error('Database test error:', error); + res.status(500).json({ + status: 'error', + error: 'Database connection failed', + details: error.message + }); + } +}); + +// Mount all routes from routes.js +app.use('/', chatRoutes); + +// Health check endpoint +app.get('/health', (req, res) => { + res.json({ status: 'healthy' }); +}); + +// Error handling middleware +app.use((err, req, res, next) => { + console.error(err.stack); + res.status(500).json({ error: 'Something broke!' }); +}); + +// Start server +app.listen(port, () => { + console.log(`Chat server running on port ${port}`); +}); \ No newline at end of file diff --git a/inventory/src/App.tsx b/inventory/src/App.tsx index aaef25e..26f6592 100644 --- a/inventory/src/App.tsx +++ b/inventory/src/App.tsx @@ -19,6 +19,7 @@ import { AuthProvider } from './contexts/AuthContext'; import { Protected } from './components/auth/Protected'; import { FirstAccessiblePage } from './components/auth/FirstAccessiblePage'; import { Brands } from '@/pages/Brands'; +import { Chat } from '@/pages/Chat'; const queryClient = new QueryClient(); function App() { @@ -133,6 +134,11 @@ function App() { } /> + + + + } /> } /> diff --git a/inventory/src/components/chat/ChatTest.tsx b/inventory/src/components/chat/ChatTest.tsx new file mode 100644 index 0000000..7630a1a --- /dev/null +++ b/inventory/src/components/chat/ChatTest.tsx @@ -0,0 +1,177 @@ +import React, { useState, useEffect } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Loader2, Hash, Lock, Users, MessageSquare } from 'lucide-react'; +import config from '@/config'; + +interface Room { + id: number; + name: string; + fname: string; + type: string; + msgs: number; + last_message_date: string; +} + +interface ChatTestProps { + selectedUserId: string; +} + +export function ChatTest({ selectedUserId }: ChatTestProps) { + const [rooms, setRooms] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (!selectedUserId) { + setRooms([]); + return; + } + + const fetchUserRooms = async () => { + setLoading(true); + setError(null); + + try { + const response = await fetch(`${config.chatUrl}/users/${selectedUserId}/rooms`); + const data = await response.json(); + + if (data.status === 'success') { + setRooms(data.rooms); + } else { + throw new Error(data.error || 'Failed to fetch rooms'); + } + } catch (err) { + console.error('Error fetching user rooms:', err); + setError(err instanceof Error ? err.message : 'An error occurred'); + } finally { + setLoading(false); + } + }; + + fetchUserRooms(); + }, [selectedUserId]); + + const getRoomIcon = (roomType: string) => { + switch (roomType) { + case 'c': + return ; + case 'p': + return ; + case 'd': + return ; + default: + return ; + } + }; + + const getRoomTypeLabel = (roomType: string) => { + switch (roomType) { + case 'c': + return 'Channel'; + case 'p': + return 'Private'; + case 'd': + return 'Direct'; + default: + return 'Unknown'; + } + }; + + if (!selectedUserId) { + return ( + + + Database Connection Test + + +

+ Select a user from the dropdown above to view their rooms and test the database connection. +

+
+
+ ); + } + + if (loading) { + return ( + + + Loading User Rooms... + + +
+ + Fetching rooms for selected user... +
+
+
+ ); + } + + if (error) { + return ( + + + Error Loading Rooms + + +

{error}

+
+
+ ); + } + + return ( + + + User Rooms ({rooms.length}) +

+ Rooms accessible to the selected user +

+
+ + {rooms.length === 0 ? ( +

No rooms found for this user.

+ ) : ( +
+ {rooms.map((room) => ( +
+
+ {getRoomIcon(room.type)} +
+
+ {room.fname || room.name || 'Unnamed Room'} +
+
+ {room.name && room.fname !== room.name && ( + #{room.name} + )} +
+
+
+ +
+ + {getRoomTypeLabel(room.type)} + + + {room.msgs} messages + + {room.last_message_date && ( + + {new Date(room.last_message_date).toLocaleDateString()} + + )} +
+
+ ))} +
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/inventory/src/components/layout/AppSidebar.tsx b/inventory/src/components/layout/AppSidebar.tsx index 10e7c15..da72243 100644 --- a/inventory/src/components/layout/AppSidebar.tsx +++ b/inventory/src/components/layout/AppSidebar.tsx @@ -9,6 +9,7 @@ import { Plus, ShoppingBag, Truck, + MessageCircle, } from "lucide-react"; import { IconCrystalBall } from "@tabler/icons-react"; import { @@ -81,6 +82,12 @@ const items = [ icon: Plus, url: "/import", permission: "access:import" + }, + { + title: "Chat", + icon: MessageCircle, + url: "/chat", + permission: "access:chat" } ]; diff --git a/inventory/src/config.ts b/inventory/src/config.ts index 0e6c871..396cf54 100644 --- a/inventory/src/config.ts +++ b/inventory/src/config.ts @@ -3,7 +3,8 @@ const isDev = import.meta.env.DEV; const config = { apiUrl: isDev ? '/api' : 'https://inventory.kent.pw/api', baseUrl: isDev ? '' : 'https://inventory.kent.pw', - authUrl: isDev ? '/auth-inv' : 'https://inventory.kent.pw/auth-inv' + authUrl: isDev ? '/auth-inv' : 'https://inventory.kent.pw/auth-inv', + chatUrl: isDev ? '/chat-api' : 'https://inventory.kent.pw/chat-api' }; export default config; \ No newline at end of file diff --git a/inventory/src/pages/Chat.tsx b/inventory/src/pages/Chat.tsx new file mode 100644 index 0000000..a61eaf2 --- /dev/null +++ b/inventory/src/pages/Chat.tsx @@ -0,0 +1,167 @@ +import React, { useState, useEffect } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Badge } from '@/components/ui/badge'; +import { Loader2, MessageCircle, Users, Database } from 'lucide-react'; +import { ChatTest } from '@/components/chat/ChatTest'; +import config from '@/config'; + +interface User { + id: number; + username: string; + name: string; + type: string; + active: boolean; +} + +interface DbStats { + active_users: number; + total_messages: number; + total_rooms: number; +} + +export function Chat() { + const [users, setUsers] = useState([]); + const [selectedUserId, setSelectedUserId] = useState(''); + const [loading, setLoading] = useState(true); + const [dbStats, setDbStats] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchInitialData = async () => { + try { + // Test database connection + const dbResponse = await fetch(`${config.chatUrl}/test-db`); + const dbData = await dbResponse.json(); + + if (dbData.status === 'success') { + setDbStats(dbData.stats); + } else { + throw new Error(dbData.error || 'Database connection failed'); + } + + // Fetch users + const usersResponse = await fetch(`${config.chatUrl}/users`); + const usersData = await usersResponse.json(); + + if (usersData.status === 'success') { + setUsers(usersData.users); + } else { + throw new Error(usersData.error || 'Failed to fetch users'); + } + } catch (err) { + console.error('Error fetching initial data:', err); + setError(err instanceof Error ? err.message : 'An error occurred'); + } finally { + setLoading(false); + } + }; + + fetchInitialData(); + }, []); + + if (loading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( +
+ + + Connection Error + + +

{error}

+

+ Make sure the chat server is running on port 3014 and the database is accessible. +

+
+
+
+ ); + } + + return ( +
+
+
+
+

Chat Archive

+

+ Read-only archive of Rocket.Chat conversations +

+
+ +
+ +
+
+ + {/* Database Stats */} + {dbStats && ( +
+ + +
+ +
+

Active Users

+

{dbStats.active_users}

+
+
+
+
+ + + +
+ +
+

Total Messages

+

{dbStats.total_messages.toLocaleString()}

+
+
+
+
+ + + +
+ +
+

Total Rooms

+

{dbStats.total_rooms}

+
+
+
+
+
+ )} +
+ + {/* Chat Test Component */} + +
+ ); +} \ No newline at end of file diff --git a/inventory/tsconfig.node.json b/inventory/tsconfig.node.json index 42872c5..d9e0c84 100644 --- a/inventory/tsconfig.node.json +++ b/inventory/tsconfig.node.json @@ -4,7 +4,8 @@ "skipLibCheck": true, "module": "ESNext", "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true, + "noEmit": true }, "include": ["vite.config.ts"] } diff --git a/inventory/vite.config.js b/inventory/vite.config.js deleted file mode 100644 index 3eecb8f..0000000 --- a/inventory/vite.config.js +++ /dev/null @@ -1,189 +0,0 @@ -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -var __generator = (this && this.__generator) || function (thisArg, body) { - var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); - return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; - function verb(n) { return function (v) { return step([n, v]); }; } - function step(op) { - if (f) throw new TypeError("Generator is already executing."); - while (g && (g = 0, op[0] && (_ = 0)), _) try { - if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; - if (y = 0, t) op = [op[0] & 2, t.value]; - switch (op[0]) { - case 0: case 1: t = op; break; - case 4: _.label++; return { value: op[1], done: false }; - case 5: _.label++; y = op[1]; op = [0]; continue; - case 7: op = _.ops.pop(); _.trys.pop(); continue; - default: - if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } - if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } - if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } - if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } - if (t[2]) _.ops.pop(); - _.trys.pop(); continue; - } - op = body.call(thisArg, _); - } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } - if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; - } -}; -import path from "path"; -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; -import { loadEnv } from "vite"; -import fs from 'fs-extra'; -// https://vitejs.dev/config/ -export default defineConfig(function (_a) { - var mode = _a.mode; - var env = loadEnv(mode, process.cwd(), ""); - var isDev = mode === 'development'; - return { - plugins: [ - react(), - { - name: 'copy-build', - closeBundle: function () { return __awaiter(void 0, void 0, void 0, function () { - var sourcePath, targetPath, error_1; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - if (!!isDev) return [3 /*break*/, 6]; - sourcePath = path.resolve(__dirname, 'build'); - targetPath = path.resolve(__dirname, '../inventory-server/frontend/build'); - _a.label = 1; - case 1: - _a.trys.push([1, 5, , 6]); - return [4 /*yield*/, fs.ensureDir(path.dirname(targetPath))]; - case 2: - _a.sent(); - return [4 /*yield*/, fs.remove(targetPath)]; - case 3: - _a.sent(); - return [4 /*yield*/, fs.copy(sourcePath, targetPath)]; - case 4: - _a.sent(); - console.log('Build files copied successfully to server directory!'); - return [3 /*break*/, 6]; - case 5: - error_1 = _a.sent(); - console.error('Error copying build files:', error_1); - process.exit(1); - return [3 /*break*/, 6]; - case 6: return [2 /*return*/]; - } - }); - }); } - } - ], - define: { - 'process.env.NODE_ENV': JSON.stringify(mode) - }, - resolve: { - alias: { - "@": path.resolve(__dirname, "./src"), - }, - }, - server: { - host: "0.0.0.0", - port: 5175, - proxy: { - "/api": { - target: "https://inventory.kent.pw", - changeOrigin: true, - secure: false, - ws: true, - xfwd: true, - cookieDomainRewrite: "", - withCredentials: true, - rewrite: function (path) { return path.replace(/^\/api/, "/api"); }, - configure: function (proxy, _options) { - proxy.on("error", function (err, req, res) { - console.log("API proxy error:", err); - res.writeHead(500, { - "Content-Type": "application/json", - }); - res.end(JSON.stringify({ error: "Proxy Error", message: err.message })); - }); - proxy.on("proxyReq", function (proxyReq, req, _res) { - console.log("Outgoing request to API:", { - method: req.method, - url: req.url, - headers: proxyReq.getHeaders(), - }); - }); - proxy.on("proxyRes", function (proxyRes, req, _res) { - console.log("API Proxy response:", { - statusCode: proxyRes.statusCode, - url: req.url, - headers: proxyRes.headers, - }); - }); - }, - }, - "/auth-inv": { - target: "https://inventory.kent.pw", - changeOrigin: true, - secure: false, - ws: true, - xfwd: true, - cookieDomainRewrite: { - "inventory.kent.pw": "localhost" - }, - withCredentials: true, - onProxyReq: function (proxyReq, req) { - // Add origin header to match CORS policy - proxyReq.setHeader('Origin', 'http://localhost:5175'); - }, - rewrite: function (path) { return path.replace(/^\/auth-inv/, "/auth-inv"); }, - configure: function (proxy, _options) { - proxy.on("error", function (err, req, res) { - console.log("Auth proxy error:", err); - res.writeHead(500, { - "Content-Type": "application/json", - }); - res.end(JSON.stringify({ error: "Proxy Error", message: err.message })); - }); - proxy.on("proxyReq", function (proxyReq, req, _res) { - console.log("Outgoing request to Auth:", { - method: req.method, - url: req.url, - headers: proxyReq.getHeaders(), - }); - }); - proxy.on("proxyRes", function (proxyRes, req, _res) { - console.log("Auth Proxy response:", { - statusCode: proxyRes.statusCode, - url: req.url, - headers: proxyRes.headers, - }); - }); - }, - }, - "/uploads": { - target: "https://inventory.kent.pw", - changeOrigin: true, - secure: false, - rewrite: function (path) { return path; }, - }, - }, - }, - build: { - outDir: "build", - sourcemap: true, - rollupOptions: { - output: { - manualChunks: { - vendor: ["react", "react-dom", "react-router-dom"], - }, - }, - }, - }, - }; -}); diff --git a/inventory/vite.config.ts b/inventory/vite.config.ts index ff8f23c..bda50cf 100644 --- a/inventory/vite.config.ts +++ b/inventory/vite.config.ts @@ -120,6 +120,41 @@ export default defineConfig(({ mode }) => { }) }, }, + "/chat-api": { + target: "https://inventory.kent.pw", + changeOrigin: true, + secure: false, + ws: true, + xfwd: true, + cookieDomainRewrite: "", + withCredentials: true, + rewrite: (path) => path, + configure: (proxy, _options) => { + proxy.on("error", (err, req, res) => { + console.log("Chat API proxy error:", err) + res.writeHead(500, { + "Content-Type": "application/json", + }) + res.end( + JSON.stringify({ error: "Proxy Error", message: err.message }) + ) + }) + proxy.on("proxyReq", (proxyReq, req, _res) => { + console.log("Outgoing request to Chat API:", { + method: req.method, + url: req.url, + headers: proxyReq.getHeaders(), + }) + }) + proxy.on("proxyRes", (proxyRes, req, _res) => { + console.log("Chat API Proxy response:", { + statusCode: proxyRes.statusCode, + url: req.url, + headers: proxyRes.headers, + }) + }) + }, + }, "/uploads": { target: "https://inventory.kent.pw", changeOrigin: true,