From 70269c40c9c250fede73d0ab34a7c79e1b0fda42 Mon Sep 17 00:00:00 2001 From: TIANHE Date: Sat, 31 Jan 2026 22:34:26 +0800 Subject: [PATCH] fix: AI analysis history user isolation & password change Turnstile bypass --- backend_api_python/app/routes/auth.py | 23 +++++++++-- .../app/routes/fast_analysis.py | 18 +++++++-- .../app/services/analysis_memory.py | 38 +++++++++++++------ .../app/services/fast_analysis.py | 18 ++++++--- .../add_user_id_to_analysis_memory.sql | 21 ++++++++++ docs/CHANGELOG.md | 14 +++++++ 6 files changed, 108 insertions(+), 24 deletions(-) create mode 100644 backend_api_python/migrations/add_user_id_to_analysis_memory.sql diff --git a/backend_api_python/app/routes/auth.py b/backend_api_python/app/routes/auth.py index 539ac00..d8bc5e9 100644 --- a/backend_api_python/app/routes/auth.py +++ b/backend_api_python/app/routes/auth.py @@ -472,10 +472,25 @@ def send_verification_code(): if not email or not email_service.is_valid_email(email): return jsonify({'code': 0, 'msg': 'Invalid email address', 'data': None}), 400 - # Verify Turnstile - turnstile_ok, turnstile_msg = security.verify_turnstile(turnstile_token, ip_address) - if not turnstile_ok: - return jsonify({'code': 0, 'msg': turnstile_msg, 'data': None}), 400 + # For change_password type with logged-in user, skip Turnstile verification + # because user already authenticated + skip_turnstile = False + if code_type == 'change_password': + # Try to get user_id from token (this route doesn't require login) + from app.utils.auth import verify_token + auth_header = request.headers.get('Authorization') + if auth_header: + parts = auth_header.split() + if len(parts) == 2 and parts[0].lower() == 'bearer': + payload = verify_token(parts[1]) + if payload and payload.get('user_id'): + skip_turnstile = True + + # Verify Turnstile (skip for authenticated change_password requests) + if not skip_turnstile: + turnstile_ok, turnstile_msg = security.verify_turnstile(turnstile_token, ip_address) + if not turnstile_ok: + return jsonify({'code': 0, 'msg': turnstile_msg, 'data': None}), 400 # Check rate limit can_send, rate_msg = security.can_send_verification_code(email, ip_address) diff --git a/backend_api_python/app/routes/fast_analysis.py b/backend_api_python/app/routes/fast_analysis.py index 9c20233..0856907 100644 --- a/backend_api_python/app/routes/fast_analysis.py +++ b/backend_api_python/app/routes/fast_analysis.py @@ -49,13 +49,17 @@ def analyze(): 'data': None }), 400 + # Get current user's ID to associate analysis with user + user_id = getattr(g, 'user_id', None) + service = get_fast_analysis_service() result = service.analyze( market=market, symbol=symbol, language=language, model=model, - timeframe=timeframe + timeframe=timeframe, + user_id=user_id ) if result.get('error'): @@ -197,8 +201,11 @@ def get_all_history(): page = int(request.args.get('page', 1)) pagesize = min(int(request.args.get('pagesize', 20)), 50) + # Get current user's ID to filter history + user_id = getattr(g, 'user_id', None) + memory = get_analysis_memory() - result = memory.get_all_history(page=page, page_size=pagesize) + result = memory.get_all_history(user_id=user_id, page=page, page_size=pagesize) return jsonify({ 'code': 1, @@ -229,8 +236,11 @@ def delete_history(memory_id: int): DELETE /api/fast-analysis/history/123 """ try: + # Get current user's ID to ensure they can only delete their own records + user_id = getattr(g, 'user_id', None) + memory = get_analysis_memory() - success = memory.delete_history(memory_id) + success = memory.delete_history(memory_id, user_id=user_id) if success: return jsonify({ @@ -241,7 +251,7 @@ def delete_history(memory_id: int): else: return jsonify({ 'code': 0, - 'msg': 'Record not found', + 'msg': 'Record not found or no permission', 'data': None }), 404 diff --git a/backend_api_python/app/services/analysis_memory.py b/backend_api_python/app/services/analysis_memory.py index 57a764d..e919c09 100644 --- a/backend_api_python/app/services/analysis_memory.py +++ b/backend_api_python/app/services/analysis_memory.py @@ -50,6 +50,7 @@ class AnalysisMemory: cur.execute(""" CREATE TABLE IF NOT EXISTS qd_analysis_memory ( id SERIAL PRIMARY KEY, + user_id INT, market VARCHAR(50) NOT NULL, symbol VARCHAR(50) NOT NULL, decision VARCHAR(10) NOT NULL, @@ -77,18 +78,22 @@ class AnalysisMemory: CREATE INDEX IF NOT EXISTS idx_analysis_memory_created ON qd_analysis_memory(created_at DESC); + + CREATE INDEX IF NOT EXISTS idx_analysis_memory_user + ON qd_analysis_memory(user_id); """) db.commit() cur.close() except Exception as e: logger.warning(f"Memory table creation skipped: {e}") - def store(self, analysis_result: Dict[str, Any]) -> Optional[int]: + def store(self, analysis_result: Dict[str, Any], user_id: int = None) -> Optional[int]: """ Store an analysis result for future reference. Args: analysis_result: Result from FastAnalysisService.analyze() + user_id: User ID who created this analysis Returns: Memory ID or None if failed @@ -115,12 +120,12 @@ class AnalysisMemory: cur.execute(""" INSERT INTO qd_analysis_memory ( - market, symbol, decision, confidence, + user_id, market, symbol, decision, confidence, price_at_analysis, entry_price, stop_loss, take_profit, summary, reasons, risks, scores, indicators_snapshot, raw_result - ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id - """, (market, symbol, decision, confidence, price, entry, stop, take, + """, (user_id, market, symbol, decision, confidence, price, entry, stop, take, summary, reasons, risks, scores, indicators, raw)) # 使用 lastrowid 属性获取 ID(execute 内部已经处理了 RETURNING) @@ -128,7 +133,7 @@ class AnalysisMemory: db.commit() cur.close() - logger.info(f"Stored analysis memory #{memory_id} for {symbol}") + logger.info(f"Stored analysis memory #{memory_id} for {symbol} by user {user_id}") return memory_id except Exception as e: @@ -192,7 +197,7 @@ class AnalysisMemory: Get all analysis history with pagination. Args: - user_id: Optional user ID filter (not used currently, for future) + user_id: User ID filter (required to show only user's own history) page: Page number (1-indexed) page_size: Items per page @@ -205,21 +210,27 @@ class AnalysisMemory: with get_db_connection() as db: cur = db.cursor() + # Build WHERE clause based on user_id + where_clause = "WHERE user_id = %s" if user_id else "" + params_count = (user_id,) if user_id else () + # Get total count - cur.execute("SELECT COUNT(*) as cnt FROM qd_analysis_memory") + cur.execute(f"SELECT COUNT(*) as cnt FROM qd_analysis_memory {where_clause}", params_count) total_row = cur.fetchone() total = total_row['cnt'] if total_row else 0 # Get paginated results - cur.execute(""" + params = (user_id, page_size, offset) if user_id else (page_size, offset) + cur.execute(f""" SELECT id, market, symbol, decision, confidence, price_at_analysis, summary, reasons, scores, indicators_snapshot, raw_result, created_at, validated_at, was_correct, actual_return_pct FROM qd_analysis_memory + {where_clause} ORDER BY created_at DESC LIMIT %s OFFSET %s - """, (page_size, offset)) + """, params) rows = cur.fetchall() or [] cur.close() @@ -254,12 +265,13 @@ class AnalysisMemory: logger.error(f"Failed to get all history: {e}") return {"items": [], "total": 0, "page": page, "page_size": page_size} - def delete_history(self, memory_id: int) -> bool: + def delete_history(self, memory_id: int, user_id: int = None) -> bool: """ Delete a history record by ID. Args: memory_id: The ID of the analysis memory to delete + user_id: User ID to ensure user can only delete their own records Returns: True if deleted successfully, False otherwise @@ -267,7 +279,11 @@ class AnalysisMemory: try: with get_db_connection() as db: cur = db.cursor() - cur.execute("DELETE FROM qd_analysis_memory WHERE id = %s", (memory_id,)) + if user_id: + # Only delete if it belongs to the user + cur.execute("DELETE FROM qd_analysis_memory WHERE id = %s AND user_id = %s", (memory_id, user_id)) + else: + cur.execute("DELETE FROM qd_analysis_memory WHERE id = %s", (memory_id,)) db.commit() affected = cur.rowcount cur.close() diff --git a/backend_api_python/app/services/fast_analysis.py b/backend_api_python/app/services/fast_analysis.py index f566d32..1633abb 100644 --- a/backend_api_python/app/services/fast_analysis.py +++ b/backend_api_python/app/services/fast_analysis.py @@ -442,10 +442,18 @@ Provide your analysis now. Remember: all prices must be within 10% of ${current_ # ==================== Main Analysis ==================== def analyze(self, market: str, symbol: str, language: str = 'en-US', - model: str = None, timeframe: str = "1D") -> Dict[str, Any]: + model: str = None, timeframe: str = "1D", user_id: int = None) -> Dict[str, Any]: """ Run fast single-call analysis. + Args: + market: Market type (Crypto, USStock, etc.) + symbol: Trading pair or stock symbol + language: Response language (zh-CN or en-US) + model: LLM model to use + timeframe: Analysis timeframe (1D, 4H, etc.) + user_id: User ID for storing analysis history + Returns: Complete analysis result with actionable recommendations. """ @@ -587,11 +595,11 @@ Provide your analysis now. Remember: all prices must be within 10% of ${current_ }) # Store in memory for future retrieval and get memory_id for feedback - memory_id = self._store_analysis_memory(result) + memory_id = self._store_analysis_memory(result, user_id=user_id) if memory_id: result["memory_id"] = memory_id - logger.info(f"Fast analysis completed in {total_time}ms: {market}:{symbol} -> {result['decision']} (memory_id={memory_id})") + logger.info(f"Fast analysis completed in {total_time}ms: {market}:{symbol} -> {result['decision']} (memory_id={memory_id}, user_id={user_id})") except Exception as e: logger.error(f"Fast analysis failed: {e}", exc_info=True) @@ -665,12 +673,12 @@ Provide your analysis now. Remember: all prices must be within 10% of ${current_ return max(0, min(100, int(overall))) - def _store_analysis_memory(self, result: Dict) -> Optional[int]: + def _store_analysis_memory(self, result: Dict, user_id: int = None) -> Optional[int]: """Store analysis result for future learning. Returns memory_id.""" try: from app.services.analysis_memory import get_analysis_memory memory = get_analysis_memory() - memory_id = memory.store(result) + memory_id = memory.store(result, user_id=user_id) return memory_id except Exception as e: logger.warning(f"Memory storage failed: {e}") diff --git a/backend_api_python/migrations/add_user_id_to_analysis_memory.sql b/backend_api_python/migrations/add_user_id_to_analysis_memory.sql new file mode 100644 index 0000000..7f648ed --- /dev/null +++ b/backend_api_python/migrations/add_user_id_to_analysis_memory.sql @@ -0,0 +1,21 @@ +-- Migration: Add user_id column to qd_analysis_memory table +-- This allows filtering analysis history by user +-- Run this migration to update existing databases + +-- Add user_id column if it doesn't exist +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'qd_analysis_memory' AND column_name = 'user_id' + ) THEN + ALTER TABLE qd_analysis_memory ADD COLUMN user_id INT; + + -- Create index for efficient user-based queries + CREATE INDEX IF NOT EXISTS idx_analysis_memory_user ON qd_analysis_memory(user_id); + + RAISE NOTICE 'Added user_id column to qd_analysis_memory'; + ELSE + RAISE NOTICE 'user_id column already exists in qd_analysis_memory'; + END IF; +END $$; diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 1c1f078..878b245 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -39,6 +39,8 @@ This document records version updates, new features, bug fixes, and database mig - Fixed A-share and H-share data fetching with multiple fallback sources - Fixed watchlist price batch fetch timeout handling - Fixed heatmap multi-language support for commodities and forex +- **Fixed AI analysis history not filtered by user** - All users were seeing the same history records; now each user only sees their own analysis history +- **Fixed "Missing Turnstile token" error when changing password** - Logged-in users no longer need Turnstile verification to request password change verification code ### 🎨 UI/UX Improvements - Reorganized left menu: Indicator Market moved below Indicator Analysis, Settings moved to bottom @@ -92,9 +94,21 @@ BEGIN END IF; END $$; +-- Add user_id column for user-specific history filtering +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'qd_analysis_memory' AND column_name = 'user_id' + ) THEN + ALTER TABLE qd_analysis_memory ADD COLUMN user_id INT; + END IF; +END $$; + CREATE INDEX IF NOT EXISTS idx_analysis_memory_symbol ON qd_analysis_memory(market, symbol); CREATE INDEX IF NOT EXISTS idx_analysis_memory_created ON qd_analysis_memory(created_at DESC); CREATE INDEX IF NOT EXISTS idx_analysis_memory_validated ON qd_analysis_memory(validated_at) WHERE validated_at IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_analysis_memory_user ON qd_analysis_memory(user_id); -- 2. Indicator Purchase Records CREATE TABLE IF NOT EXISTS qd_indicator_purchases (