fix: AI analysis history user isolation & password change Turnstile bypass
This commit is contained in:
@@ -472,10 +472,25 @@ def send_verification_code():
|
|||||||
if not email or not email_service.is_valid_email(email):
|
if not email or not email_service.is_valid_email(email):
|
||||||
return jsonify({'code': 0, 'msg': 'Invalid email address', 'data': None}), 400
|
return jsonify({'code': 0, 'msg': 'Invalid email address', 'data': None}), 400
|
||||||
|
|
||||||
# Verify Turnstile
|
# For change_password type with logged-in user, skip Turnstile verification
|
||||||
turnstile_ok, turnstile_msg = security.verify_turnstile(turnstile_token, ip_address)
|
# because user already authenticated
|
||||||
if not turnstile_ok:
|
skip_turnstile = False
|
||||||
return jsonify({'code': 0, 'msg': turnstile_msg, 'data': None}), 400
|
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
|
# Check rate limit
|
||||||
can_send, rate_msg = security.can_send_verification_code(email, ip_address)
|
can_send, rate_msg = security.can_send_verification_code(email, ip_address)
|
||||||
|
|||||||
@@ -49,13 +49,17 @@ def analyze():
|
|||||||
'data': None
|
'data': None
|
||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
|
# Get current user's ID to associate analysis with user
|
||||||
|
user_id = getattr(g, 'user_id', None)
|
||||||
|
|
||||||
service = get_fast_analysis_service()
|
service = get_fast_analysis_service()
|
||||||
result = service.analyze(
|
result = service.analyze(
|
||||||
market=market,
|
market=market,
|
||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
language=language,
|
language=language,
|
||||||
model=model,
|
model=model,
|
||||||
timeframe=timeframe
|
timeframe=timeframe,
|
||||||
|
user_id=user_id
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.get('error'):
|
if result.get('error'):
|
||||||
@@ -197,8 +201,11 @@ def get_all_history():
|
|||||||
page = int(request.args.get('page', 1))
|
page = int(request.args.get('page', 1))
|
||||||
pagesize = min(int(request.args.get('pagesize', 20)), 50)
|
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()
|
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({
|
return jsonify({
|
||||||
'code': 1,
|
'code': 1,
|
||||||
@@ -229,8 +236,11 @@ def delete_history(memory_id: int):
|
|||||||
DELETE /api/fast-analysis/history/123
|
DELETE /api/fast-analysis/history/123
|
||||||
"""
|
"""
|
||||||
try:
|
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()
|
memory = get_analysis_memory()
|
||||||
success = memory.delete_history(memory_id)
|
success = memory.delete_history(memory_id, user_id=user_id)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@@ -241,7 +251,7 @@ def delete_history(memory_id: int):
|
|||||||
else:
|
else:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'code': 0,
|
'code': 0,
|
||||||
'msg': 'Record not found',
|
'msg': 'Record not found or no permission',
|
||||||
'data': None
|
'data': None
|
||||||
}), 404
|
}), 404
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ class AnalysisMemory:
|
|||||||
cur.execute("""
|
cur.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS qd_analysis_memory (
|
CREATE TABLE IF NOT EXISTS qd_analysis_memory (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INT,
|
||||||
market VARCHAR(50) NOT NULL,
|
market VARCHAR(50) NOT NULL,
|
||||||
symbol VARCHAR(50) NOT NULL,
|
symbol VARCHAR(50) NOT NULL,
|
||||||
decision VARCHAR(10) NOT NULL,
|
decision VARCHAR(10) NOT NULL,
|
||||||
@@ -77,18 +78,22 @@ class AnalysisMemory:
|
|||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_analysis_memory_created
|
CREATE INDEX IF NOT EXISTS idx_analysis_memory_created
|
||||||
ON qd_analysis_memory(created_at DESC);
|
ON qd_analysis_memory(created_at DESC);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_analysis_memory_user
|
||||||
|
ON qd_analysis_memory(user_id);
|
||||||
""")
|
""")
|
||||||
db.commit()
|
db.commit()
|
||||||
cur.close()
|
cur.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Memory table creation skipped: {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.
|
Store an analysis result for future reference.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
analysis_result: Result from FastAnalysisService.analyze()
|
analysis_result: Result from FastAnalysisService.analyze()
|
||||||
|
user_id: User ID who created this analysis
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Memory ID or None if failed
|
Memory ID or None if failed
|
||||||
@@ -115,12 +120,12 @@ class AnalysisMemory:
|
|||||||
|
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
INSERT INTO qd_analysis_memory (
|
INSERT INTO qd_analysis_memory (
|
||||||
market, symbol, decision, confidence,
|
user_id, market, symbol, decision, confidence,
|
||||||
price_at_analysis, entry_price, stop_loss, take_profit,
|
price_at_analysis, entry_price, stop_loss, take_profit,
|
||||||
summary, reasons, risks, scores, indicators_snapshot, raw_result
|
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
|
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))
|
summary, reasons, risks, scores, indicators, raw))
|
||||||
|
|
||||||
# 使用 lastrowid 属性获取 ID(execute 内部已经处理了 RETURNING)
|
# 使用 lastrowid 属性获取 ID(execute 内部已经处理了 RETURNING)
|
||||||
@@ -128,7 +133,7 @@ class AnalysisMemory:
|
|||||||
db.commit()
|
db.commit()
|
||||||
cur.close()
|
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
|
return memory_id
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -192,7 +197,7 @@ class AnalysisMemory:
|
|||||||
Get all analysis history with pagination.
|
Get all analysis history with pagination.
|
||||||
|
|
||||||
Args:
|
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: Page number (1-indexed)
|
||||||
page_size: Items per page
|
page_size: Items per page
|
||||||
|
|
||||||
@@ -205,21 +210,27 @@ class AnalysisMemory:
|
|||||||
with get_db_connection() as db:
|
with get_db_connection() as db:
|
||||||
cur = db.cursor()
|
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
|
# 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_row = cur.fetchone()
|
||||||
total = total_row['cnt'] if total_row else 0
|
total = total_row['cnt'] if total_row else 0
|
||||||
|
|
||||||
# Get paginated results
|
# Get paginated results
|
||||||
cur.execute("""
|
params = (user_id, page_size, offset) if user_id else (page_size, offset)
|
||||||
|
cur.execute(f"""
|
||||||
SELECT
|
SELECT
|
||||||
id, market, symbol, decision, confidence, price_at_analysis,
|
id, market, symbol, decision, confidence, price_at_analysis,
|
||||||
summary, reasons, scores, indicators_snapshot, raw_result,
|
summary, reasons, scores, indicators_snapshot, raw_result,
|
||||||
created_at, validated_at, was_correct, actual_return_pct
|
created_at, validated_at, was_correct, actual_return_pct
|
||||||
FROM qd_analysis_memory
|
FROM qd_analysis_memory
|
||||||
|
{where_clause}
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
LIMIT %s OFFSET %s
|
LIMIT %s OFFSET %s
|
||||||
""", (page_size, offset))
|
""", params)
|
||||||
|
|
||||||
rows = cur.fetchall() or []
|
rows = cur.fetchall() or []
|
||||||
cur.close()
|
cur.close()
|
||||||
@@ -254,12 +265,13 @@ class AnalysisMemory:
|
|||||||
logger.error(f"Failed to get all history: {e}")
|
logger.error(f"Failed to get all history: {e}")
|
||||||
return {"items": [], "total": 0, "page": page, "page_size": page_size}
|
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.
|
Delete a history record by ID.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
memory_id: The ID of the analysis memory to delete
|
memory_id: The ID of the analysis memory to delete
|
||||||
|
user_id: User ID to ensure user can only delete their own records
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if deleted successfully, False otherwise
|
True if deleted successfully, False otherwise
|
||||||
@@ -267,7 +279,11 @@ class AnalysisMemory:
|
|||||||
try:
|
try:
|
||||||
with get_db_connection() as db:
|
with get_db_connection() as db:
|
||||||
cur = db.cursor()
|
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()
|
db.commit()
|
||||||
affected = cur.rowcount
|
affected = cur.rowcount
|
||||||
cur.close()
|
cur.close()
|
||||||
|
|||||||
@@ -442,10 +442,18 @@ Provide your analysis now. Remember: all prices must be within 10% of ${current_
|
|||||||
# ==================== Main Analysis ====================
|
# ==================== Main Analysis ====================
|
||||||
|
|
||||||
def analyze(self, market: str, symbol: str, language: str = 'en-US',
|
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.
|
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:
|
Returns:
|
||||||
Complete analysis result with actionable recommendations.
|
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
|
# 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:
|
if memory_id:
|
||||||
result["memory_id"] = 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:
|
except Exception as e:
|
||||||
logger.error(f"Fast analysis failed: {e}", exc_info=True)
|
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)))
|
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."""
|
"""Store analysis result for future learning. Returns memory_id."""
|
||||||
try:
|
try:
|
||||||
from app.services.analysis_memory import get_analysis_memory
|
from app.services.analysis_memory import get_analysis_memory
|
||||||
memory = get_analysis_memory()
|
memory = get_analysis_memory()
|
||||||
memory_id = memory.store(result)
|
memory_id = memory.store(result, user_id=user_id)
|
||||||
return memory_id
|
return memory_id
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Memory storage failed: {e}")
|
logger.warning(f"Memory storage failed: {e}")
|
||||||
|
|||||||
@@ -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 $$;
|
||||||
@@ -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 A-share and H-share data fetching with multiple fallback sources
|
||||||
- Fixed watchlist price batch fetch timeout handling
|
- Fixed watchlist price batch fetch timeout handling
|
||||||
- Fixed heatmap multi-language support for commodities and forex
|
- 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
|
### 🎨 UI/UX Improvements
|
||||||
- Reorganized left menu: Indicator Market moved below Indicator Analysis, Settings moved to bottom
|
- Reorganized left menu: Indicator Market moved below Indicator Analysis, Settings moved to bottom
|
||||||
@@ -92,9 +94,21 @@ BEGIN
|
|||||||
END IF;
|
END IF;
|
||||||
END $$;
|
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_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_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_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
|
-- 2. Indicator Purchase Records
|
||||||
CREATE TABLE IF NOT EXISTS qd_indicator_purchases (
|
CREATE TABLE IF NOT EXISTS qd_indicator_purchases (
|
||||||
|
|||||||
Reference in New Issue
Block a user