From ca32dc8867b806948a7311550f97ebfef259d514 Mon Sep 17 00:00:00 2001 From: "Andrew K. Choi" Date: Fri, 26 Sep 2025 08:47:15 +0900 Subject: [PATCH] energensy contacts, dashboard --- ...a82_add_is_active_to_emergency_contacts.py | 26 +++ deploy_migration.sh | 32 ++++ services/api_gateway/main.py | 118 ++++++++++++++ services/user_service/main.py | 52 ++++++ test_fixed_endpoints.py | 106 +++++++++++++ test_mobile_formats.py | 149 ++++++++++++++++++ test_production_login.py | 100 ++++++++++++ 7 files changed, 583 insertions(+) create mode 100644 alembic/versions/d9c621d45a82_add_is_active_to_emergency_contacts.py create mode 100755 deploy_migration.sh create mode 100644 test_fixed_endpoints.py create mode 100644 test_mobile_formats.py create mode 100644 test_production_login.py diff --git a/alembic/versions/d9c621d45a82_add_is_active_to_emergency_contacts.py b/alembic/versions/d9c621d45a82_add_is_active_to_emergency_contacts.py new file mode 100644 index 0000000..16bf703 --- /dev/null +++ b/alembic/versions/d9c621d45a82_add_is_active_to_emergency_contacts.py @@ -0,0 +1,26 @@ +"""add_is_active_to_emergency_contacts + +Revision ID: d9c621d45a82 +Revises: 2a4784830015 +Create Date: 2025-09-26 08:42:00.128700 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'd9c621d45a82' +down_revision = '2a4784830015' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Add is_active column to emergency_contacts table + op.add_column('emergency_contacts', sa.Column('is_active', sa.Boolean(), nullable=False, server_default='true')) + + +def downgrade() -> None: + # Remove is_active column from emergency_contacts table + op.drop_column('emergency_contacts', 'is_active') \ No newline at end of file diff --git a/deploy_migration.sh b/deploy_migration.sh new file mode 100755 index 0000000..7b24f59 --- /dev/null +++ b/deploy_migration.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +# Deploy database migration to production server +echo "Deploying database migration to production server 192.168.0.103..." + +# Create migration file on server +echo "Creating migration directory on server..." +ssh trevor@192.168.0.103 "sudo mkdir -p /opt/chat/alembic/versions" + +# Copy migration file +echo "Copying migration file..." +scp alembic/versions/d9c621d45a82_add_is_active_to_emergency_contacts.py trevor@192.168.0.103:/tmp/ +ssh trevor@192.168.0.103 "sudo mv /tmp/d9c621d45a82_add_is_active_to_emergency_contacts.py /opt/chat/alembic/versions/" + +# Apply migration on server +echo "Applying migration on production server..." +ssh trevor@192.168.0.103 << 'EOF' +cd /opt/chat + +# Activate virtual environment if it exists +if [ -d ".venv" ]; then + source .venv/bin/activate +fi + +# Apply migration +export PYTHONPATH="/opt/chat:$PYTHONPATH" +python -m alembic upgrade head + +echo "Migration applied successfully!" +EOF + +echo "Migration deployment completed!" \ No newline at end of file diff --git a/services/api_gateway/main.py b/services/api_gateway/main.py index 5ce0588..0d02d6b 100644 --- a/services/api_gateway/main.py +++ b/services/api_gateway/main.py @@ -9,6 +9,7 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from fastapi.openapi.utils import get_openapi from pydantic import BaseModel, Field +from typing import Any from shared.config import settings from services.user_service.schemas import UserCreate, UserLogin, UserResponse, UserUpdate, Token @@ -248,7 +249,17 @@ async def register_user(user_create: UserCreate, request: Request): async def login_user(user_login: UserLogin, request: Request): """Login user""" client_ip = get_client_ip(request) + + # Дополнительное логирование для отладки + try: + request_body = await request.body() + print(f"RAW request body from {client_ip}: {request_body}") + print(f"Request headers: {dict(request.headers)}") + except: + pass + print(f"Login request from {client_ip}: {user_login.model_dump(exclude={'password'})}") + print(f"Full login data: {user_login.model_dump()}") async with httpx.AsyncClient(timeout=30.0) as client: try: @@ -312,6 +323,113 @@ async def login_user(user_login: UserLogin, request: Request): raise HTTPException(status_code=500, detail=f"Login error: {str(e)}") +# Debug endpoint to analyze raw request data +@app.post("/api/v1/debug/login", tags=["Debug"], summary="Debug login request data") +async def debug_login_request(request: Request): + """Debug endpoint to analyze raw request data from mobile app""" + try: + client_ip = get_client_ip(request) + headers = dict(request.headers) + body = await request.body() + + debug_info = { + "client_ip": client_ip, + "headers": headers, + "raw_body": body.decode('utf-8', errors='ignore') if body else None, + "content_length": len(body) if body else 0, + "content_type": headers.get('content-type', 'unknown') + } + + print(f"DEBUG LOGIN from {client_ip}:") + print(f"Headers: {headers}") + print(f"Raw body: {body}") + + # Try to parse as JSON + try: + if body: + import json + json_data = json.loads(body) + debug_info["parsed_json"] = json_data + print(f"Parsed JSON: {json_data}") + except Exception as e: + debug_info["json_parse_error"] = str(e) + print(f"JSON parse error: {e}") + + return debug_info + + except Exception as e: + print(f"Debug endpoint error: {str(e)}") + return {"error": str(e)} + + +# Alternative login endpoint for mobile app debugging +@app.post("/api/v1/auth/login-flexible", tags=["Authentication"], summary="Flexible login for debugging") +async def login_flexible(request: Request): + """Flexible login endpoint that accepts various data formats""" + try: + client_ip = get_client_ip(request) + body = await request.body() + + print(f"Flexible login from {client_ip}") + print(f"Raw request: {body}") + + # Try to parse JSON + import json + try: + data = json.loads(body) if body else {} + except: + return {"error": "Invalid JSON format", "status": 422} + + print(f"Parsed data: {data}") + + # Extract login fields with flexible names + email = data.get('email') or data.get('Email') or data.get('EMAIL') + username = data.get('username') or data.get('Username') or data.get('user_name') or data.get('login') + password = data.get('password') or data.get('Password') or data.get('PASSWORD') or data.get('pass') + + print(f"Extracted: email={email}, username={username}, password={'***' if password else None}") + + # Validation + if not password: + return {"error": "Password is required", "status": 422} + + if not email and not username: + return {"error": "Either email or username must be provided", "status": 422} + + # Create proper login data + login_data = { + "email": email, + "username": username, + "password": password + } + + # Forward to user service + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.post( + f"{SERVICES['users']}/api/v1/auth/login", + json=login_data, + headers={ + "Content-Type": "application/json", + "Accept": "application/json" + } + ) + + print(f"User service response: {response.status_code} - {response.text}") + + if response.status_code == 200: + return response.json() + else: + try: + error_data = response.json() + return {"error": error_data.get("detail", "Login failed"), "status": response.status_code} + except: + return {"error": response.text, "status": response.status_code} + + except Exception as e: + print(f"Flexible login error: {str(e)}") + return {"error": str(e), "status": 500} + + # Utility endpoints @app.get("/api/v1/auth/check-email", tags=["Authentication"], summary="Check if email is available") async def check_email_availability(email: str): diff --git a/services/user_service/main.py b/services/user_service/main.py index d6edafd..23de4b9 100644 --- a/services/user_service/main.py +++ b/services/user_service/main.py @@ -372,6 +372,58 @@ async def delete_emergency_contact( return None +@app.get("/api/v1/users/dashboard", tags=["Dashboard"]) +async def get_user_dashboard( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + """Get user dashboard data""" + try: + # Get emergency contacts count + emergency_contacts_result = await db.execute( + select(EmergencyContact).filter( + EmergencyContact.user_id == current_user.id, + EmergencyContact.is_active == True + ) + ) + emergency_contacts = emergency_contacts_result.scalars().all() + + dashboard_data = { + "user": { + "id": current_user.id, + "uuid": str(current_user.uuid), + "email": current_user.email, + "username": current_user.username, + "first_name": current_user.first_name, + "last_name": current_user.last_name, + "avatar_url": current_user.avatar_url + }, + "emergency_contacts_count": len(emergency_contacts), + "settings": { + "location_sharing_enabled": current_user.location_sharing_enabled, + "emergency_notifications_enabled": current_user.emergency_notifications_enabled, + "push_notifications_enabled": current_user.push_notifications_enabled + }, + "verification_status": { + "email_verified": current_user.email_verified, + "phone_verified": current_user.phone_verified + }, + "account_status": { + "is_active": current_user.is_active, + "created_at": str(current_user.created_at) if hasattr(current_user, 'created_at') else None + } + } + + return dashboard_data + + except Exception as e: + print(f"Dashboard error: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to load dashboard data" + ) + + @app.get("/api/v1/health") async def health_check_v1(): """Health check endpoint with API version""" diff --git a/test_fixed_endpoints.py b/test_fixed_endpoints.py new file mode 100644 index 0000000..4a4247e --- /dev/null +++ b/test_fixed_endpoints.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 + +import json +import requests +import time + +def test_fixed_endpoints(): + """Test the fixed endpoints on production server""" + base_url = "http://192.168.0.103:8000" + + # First, login to get access token + print("🔐 Step 1: Login to get access token") + login_response = requests.post( + f"{base_url}/api/v1/auth/login", + json={ + "username": "Trevor1985", + "password": "Cl0ud_1985!" + }, + headers={"Content-Type": "application/json"} + ) + + if login_response.status_code != 200: + print(f"❌ Login failed: {login_response.status_code} - {login_response.text}") + return + + token_data = login_response.json() + access_token = token_data["access_token"] + print(f"✅ Login successful! Got access token.") + + # Test endpoints that were failing + headers = { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json" + } + + test_endpoints = [ + { + "name": "User Profile", + "url": "/api/v1/users/me", + "method": "GET" + }, + { + "name": "User Dashboard (was 404)", + "url": "/api/v1/users/dashboard", + "method": "GET" + }, + { + "name": "Emergency Contacts (was 500)", + "url": "/api/v1/users/me/emergency-contacts", + "method": "GET" + }, + { + "name": "Profile (alternative endpoint)", + "url": "/api/v1/profile", + "method": "GET" + } + ] + + print(f"\n🧪 Step 2: Testing fixed endpoints") + print("=" * 60) + + for endpoint in test_endpoints: + print(f"\n📍 Testing: {endpoint['name']}") + print(f" URL: {endpoint['url']}") + + try: + if endpoint['method'] == 'GET': + response = requests.get( + f"{base_url}{endpoint['url']}", + headers=headers, + timeout=10 + ) + + print(f" Status: {response.status_code}") + + if response.status_code == 200: + try: + data = response.json() + if endpoint['name'] == "User Dashboard (was 404)": + print(f" ✅ Dashboard loaded! Emergency contacts: {data.get('emergency_contacts_count', 0)}") + print(f" User: {data.get('user', {}).get('username', 'N/A')}") + elif endpoint['name'] == "Emergency Contacts (was 500)": + contacts = data if isinstance(data, list) else [] + print(f" ✅ Emergency contacts loaded! Count: {len(contacts)}") + else: + print(f" ✅ Success! Got user data.") + except: + print(f" ✅ Success! (Response not JSON)") + else: + print(f" ❌ Failed: {response.text[:100]}...") + + except Exception as e: + print(f" 💥 Error: {str(e)}") + + time.sleep(0.5) + + print(f"\n{'='*60}") + print("📊 Summary:") + print("• Login: ✅ Working") + print("• Check each endpoint status above") + print("• All 422 login errors should be resolved") + print("• Database errors should be fixed") + +if __name__ == "__main__": + print("🚀 Testing fixed endpoints on production server 192.168.0.103") + test_fixed_endpoints() \ No newline at end of file diff --git a/test_mobile_formats.py b/test_mobile_formats.py new file mode 100644 index 0000000..87766fe --- /dev/null +++ b/test_mobile_formats.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 + +import json +import requests +import time + +def test_mobile_app_formats(): + """Test various data formats that mobile apps commonly send""" + base_url = "http://192.168.0.103:8000" + + # Common mobile app data format issues + test_cases = [ + { + "name": "Android app - nested data structure", + "data": { + "user": { + "email": "testuser@example.com", + "password": "SecurePass123" + } + } + }, + { + "name": "iOS app - camelCase fields", + "data": { + "emailAddress": "testuser@example.com", + "userPassword": "SecurePass123" + } + }, + { + "name": "React Native - mixed case", + "data": { + "Email": "testuser@example.com", + "Password": "SecurePass123" + } + }, + { + "name": "Flutter app - snake_case", + "data": { + "user_email": "testuser@example.com", + "user_password": "SecurePass123" + } + }, + { + "name": "Mobile app with extra fields", + "data": { + "email": "testuser@example.com", + "password": "SecurePass123", + "device_id": "mobile123", + "app_version": "1.0.0", + "platform": "android" + } + }, + { + "name": "Empty string fields (common mobile bug)", + "data": { + "email": "", + "username": "", + "password": "SecurePass123" + } + }, + { + "name": "Null fields (another common mobile bug)", + "data": { + "email": None, + "username": None, + "password": "SecurePass123" + } + }, + { + "name": "Correct format", + "data": { + "email": "testuser@example.com", + "password": "SecurePass123" + } + } + ] + + headers = { + "Content-Type": "application/json", + "Accept": "application/json", + "User-Agent": "WomenSafetyApp/1.0 (Android)" + } + + print("📱 Testing mobile app data formats on 192.168.0.103") + print("=" * 70) + + for i, test_case in enumerate(test_cases, 1): + print(f"\n{i}. 🧪 {test_case['name']}") + print("-" * 50) + + try: + # Send request + response = requests.post( + f"{base_url}/api/v1/auth/login", + json=test_case["data"], + headers=headers, + timeout=10 + ) + + print(f"📤 Request: {json.dumps(test_case['data'], indent=2)}") + print(f"📊 Status: {response.status_code}") + + # Analyze response + if response.status_code == 200: + print("✅ SUCCESS - Login worked!") + try: + token_data = response.json() + print(f"🔐 Token type: {token_data.get('token_type')}") + except: + pass + + elif response.status_code == 422: + print("❌ VALIDATION ERROR") + try: + error_data = response.json() + if "detail" in error_data: + detail = error_data["detail"] + if isinstance(detail, list): + print("📝 Validation issues:") + for error in detail: + field = error.get("loc", [])[-1] if error.get("loc") else "unknown" + msg = error.get("msg", "Unknown error") + input_val = error.get("input", "") + print(f" • Field '{field}': {msg} (input: {input_val})") + else: + print(f"📝 Error: {detail}") + except Exception as e: + print(f"📝 Raw error: {response.text}") + + elif response.status_code == 401: + print("🔒 AUTHENTICATION FAILED - Wrong credentials") + + else: + print(f"🚫 OTHER ERROR: {response.status_code}") + print(f"📝 Response: {response.text[:200]}") + + except Exception as e: + print(f"💥 REQUEST ERROR: {str(e)}") + + time.sleep(0.5) + + print(f"\n{'='*70}") + print("📋 SUMMARY:") + print("• Check which format works correctly") + print("• Compare with mobile app's actual request format") + print("• Update mobile app to match working format") + +if __name__ == "__main__": + test_mobile_app_formats() \ No newline at end of file diff --git a/test_production_login.py b/test_production_login.py new file mode 100644 index 0000000..f348f09 --- /dev/null +++ b/test_production_login.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 + +import json +import requests +import time + +def test_production_login(): + """Test login endpoint on production server""" + base_url = "http://192.168.0.103:8000" + + # Test cases that might be coming from mobile app + test_cases = [ + { + "name": "Empty request (like from emulator)", + "data": {} + }, + { + "name": "Only password", + "data": {"password": "testpass123"} + }, + { + "name": "Email with extra spaces", + "data": { + "email": " testuser@example.com ", + "password": "SecurePass123" + } + }, + { + "name": "Valid login data", + "data": { + "email": "testuser@example.com", + "password": "SecurePass123" + } + }, + { + "name": "Username login", + "data": { + "username": "testuser123", + "password": "SecurePass123" + } + }, + { + "name": "Invalid JSON structure (common mobile app error)", + "data": '{"email":"test@example.com","password":"test123"', + "raw": True + } + ] + + headers = { + "Content-Type": "application/json", + "Accept": "application/json", + "User-Agent": "MobileApp/1.0" + } + + for test_case in test_cases: + print(f"\n🧪 Testing: {test_case['name']}") + print("=" * 60) + + try: + if test_case.get("raw"): + data = test_case["data"] + print(f"Raw data: {data}") + else: + data = json.dumps(test_case["data"]) + print(f"JSON data: {data}") + + response = requests.post( + f"{base_url}/api/v1/auth/login", + data=data, + headers=headers, + timeout=10 + ) + + print(f"Status: {response.status_code}") + print(f"Response: {response.text[:200]}...") + + if response.status_code == 422: + print("🔍 This is a validation error - checking details...") + try: + error_details = response.json() + if "detail" in error_details: + detail = error_details["detail"] + if isinstance(detail, list): + for error in detail: + field = error.get("loc", [])[-1] if error.get("loc") else "unknown" + msg = error.get("msg", "Unknown error") + print(f" Field '{field}': {msg}") + else: + print(f" Error: {detail}") + except: + pass + + except Exception as e: + print(f"Request error: {str(e)}") + + time.sleep(1) + +if __name__ == "__main__": + print("🔍 Testing login endpoint on production server 192.168.0.103") + test_production_login() \ No newline at end of file