Skip to content

Commit 70269c4

Browse files
committed
fix: AI analysis history user isolation & password change Turnstile bypass
1 parent d5e023a commit 70269c4

File tree

6 files changed

+108
-24
lines changed

6 files changed

+108
-24
lines changed

backend_api_python/app/routes/auth.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -472,10 +472,25 @@ def send_verification_code():
472472
if not email or not email_service.is_valid_email(email):
473473
return jsonify({'code': 0, 'msg': 'Invalid email address', 'data': None}), 400
474474

475-
# Verify Turnstile
476-
turnstile_ok, turnstile_msg = security.verify_turnstile(turnstile_token, ip_address)
477-
if not turnstile_ok:
478-
return jsonify({'code': 0, 'msg': turnstile_msg, 'data': None}), 400
475+
# For change_password type with logged-in user, skip Turnstile verification
476+
# because user already authenticated
477+
skip_turnstile = False
478+
if code_type == 'change_password':
479+
# Try to get user_id from token (this route doesn't require login)
480+
from app.utils.auth import verify_token
481+
auth_header = request.headers.get('Authorization')
482+
if auth_header:
483+
parts = auth_header.split()
484+
if len(parts) == 2 and parts[0].lower() == 'bearer':
485+
payload = verify_token(parts[1])
486+
if payload and payload.get('user_id'):
487+
skip_turnstile = True
488+
489+
# Verify Turnstile (skip for authenticated change_password requests)
490+
if not skip_turnstile:
491+
turnstile_ok, turnstile_msg = security.verify_turnstile(turnstile_token, ip_address)
492+
if not turnstile_ok:
493+
return jsonify({'code': 0, 'msg': turnstile_msg, 'data': None}), 400
479494

480495
# Check rate limit
481496
can_send, rate_msg = security.can_send_verification_code(email, ip_address)

backend_api_python/app/routes/fast_analysis.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,17 @@ def analyze():
4949
'data': None
5050
}), 400
5151

52+
# Get current user's ID to associate analysis with user
53+
user_id = getattr(g, 'user_id', None)
54+
5255
service = get_fast_analysis_service()
5356
result = service.analyze(
5457
market=market,
5558
symbol=symbol,
5659
language=language,
5760
model=model,
58-
timeframe=timeframe
61+
timeframe=timeframe,
62+
user_id=user_id
5963
)
6064

6165
if result.get('error'):
@@ -197,8 +201,11 @@ def get_all_history():
197201
page = int(request.args.get('page', 1))
198202
pagesize = min(int(request.args.get('pagesize', 20)), 50)
199203

204+
# Get current user's ID to filter history
205+
user_id = getattr(g, 'user_id', None)
206+
200207
memory = get_analysis_memory()
201-
result = memory.get_all_history(page=page, page_size=pagesize)
208+
result = memory.get_all_history(user_id=user_id, page=page, page_size=pagesize)
202209

203210
return jsonify({
204211
'code': 1,
@@ -229,8 +236,11 @@ def delete_history(memory_id: int):
229236
DELETE /api/fast-analysis/history/123
230237
"""
231238
try:
239+
# Get current user's ID to ensure they can only delete their own records
240+
user_id = getattr(g, 'user_id', None)
241+
232242
memory = get_analysis_memory()
233-
success = memory.delete_history(memory_id)
243+
success = memory.delete_history(memory_id, user_id=user_id)
234244

235245
if success:
236246
return jsonify({
@@ -241,7 +251,7 @@ def delete_history(memory_id: int):
241251
else:
242252
return jsonify({
243253
'code': 0,
244-
'msg': 'Record not found',
254+
'msg': 'Record not found or no permission',
245255
'data': None
246256
}), 404
247257

backend_api_python/app/services/analysis_memory.py

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def _ensure_table(self):
5050
cur.execute("""
5151
CREATE TABLE IF NOT EXISTS qd_analysis_memory (
5252
id SERIAL PRIMARY KEY,
53+
user_id INT,
5354
market VARCHAR(50) NOT NULL,
5455
symbol VARCHAR(50) NOT NULL,
5556
decision VARCHAR(10) NOT NULL,
@@ -77,18 +78,22 @@ def _ensure_table(self):
7778
7879
CREATE INDEX IF NOT EXISTS idx_analysis_memory_created
7980
ON qd_analysis_memory(created_at DESC);
81+
82+
CREATE INDEX IF NOT EXISTS idx_analysis_memory_user
83+
ON qd_analysis_memory(user_id);
8084
""")
8185
db.commit()
8286
cur.close()
8387
except Exception as e:
8488
logger.warning(f"Memory table creation skipped: {e}")
8589

86-
def store(self, analysis_result: Dict[str, Any]) -> Optional[int]:
90+
def store(self, analysis_result: Dict[str, Any], user_id: int = None) -> Optional[int]:
8791
"""
8892
Store an analysis result for future reference.
8993
9094
Args:
9195
analysis_result: Result from FastAnalysisService.analyze()
96+
user_id: User ID who created this analysis
9297
9398
Returns:
9499
Memory ID or None if failed
@@ -115,20 +120,20 @@ def store(self, analysis_result: Dict[str, Any]) -> Optional[int]:
115120

116121
cur.execute("""
117122
INSERT INTO qd_analysis_memory (
118-
market, symbol, decision, confidence,
123+
user_id, market, symbol, decision, confidence,
119124
price_at_analysis, entry_price, stop_loss, take_profit,
120125
summary, reasons, risks, scores, indicators_snapshot, raw_result
121-
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
126+
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
122127
RETURNING id
123-
""", (market, symbol, decision, confidence, price, entry, stop, take,
128+
""", (user_id, market, symbol, decision, confidence, price, entry, stop, take,
124129
summary, reasons, risks, scores, indicators, raw))
125130

126131
# 使用 lastrowid 属性获取 ID(execute 内部已经处理了 RETURNING)
127132
memory_id = cur.lastrowid
128133
db.commit()
129134
cur.close()
130135

131-
logger.info(f"Stored analysis memory #{memory_id} for {symbol}")
136+
logger.info(f"Stored analysis memory #{memory_id} for {symbol} by user {user_id}")
132137
return memory_id
133138

134139
except Exception as e:
@@ -192,7 +197,7 @@ def get_all_history(self, user_id: int = None, page: int = 1, page_size: int = 2
192197
Get all analysis history with pagination.
193198
194199
Args:
195-
user_id: Optional user ID filter (not used currently, for future)
200+
user_id: User ID filter (required to show only user's own history)
196201
page: Page number (1-indexed)
197202
page_size: Items per page
198203
@@ -205,21 +210,27 @@ def get_all_history(self, user_id: int = None, page: int = 1, page_size: int = 2
205210
with get_db_connection() as db:
206211
cur = db.cursor()
207212

213+
# Build WHERE clause based on user_id
214+
where_clause = "WHERE user_id = %s" if user_id else ""
215+
params_count = (user_id,) if user_id else ()
216+
208217
# Get total count
209-
cur.execute("SELECT COUNT(*) as cnt FROM qd_analysis_memory")
218+
cur.execute(f"SELECT COUNT(*) as cnt FROM qd_analysis_memory {where_clause}", params_count)
210219
total_row = cur.fetchone()
211220
total = total_row['cnt'] if total_row else 0
212221

213222
# Get paginated results
214-
cur.execute("""
223+
params = (user_id, page_size, offset) if user_id else (page_size, offset)
224+
cur.execute(f"""
215225
SELECT
216226
id, market, symbol, decision, confidence, price_at_analysis,
217227
summary, reasons, scores, indicators_snapshot, raw_result,
218228
created_at, validated_at, was_correct, actual_return_pct
219229
FROM qd_analysis_memory
230+
{where_clause}
220231
ORDER BY created_at DESC
221232
LIMIT %s OFFSET %s
222-
""", (page_size, offset))
233+
""", params)
223234

224235
rows = cur.fetchall() or []
225236
cur.close()
@@ -254,20 +265,25 @@ def get_all_history(self, user_id: int = None, page: int = 1, page_size: int = 2
254265
logger.error(f"Failed to get all history: {e}")
255266
return {"items": [], "total": 0, "page": page, "page_size": page_size}
256267

257-
def delete_history(self, memory_id: int) -> bool:
268+
def delete_history(self, memory_id: int, user_id: int = None) -> bool:
258269
"""
259270
Delete a history record by ID.
260271
261272
Args:
262273
memory_id: The ID of the analysis memory to delete
274+
user_id: User ID to ensure user can only delete their own records
263275
264276
Returns:
265277
True if deleted successfully, False otherwise
266278
"""
267279
try:
268280
with get_db_connection() as db:
269281
cur = db.cursor()
270-
cur.execute("DELETE FROM qd_analysis_memory WHERE id = %s", (memory_id,))
282+
if user_id:
283+
# Only delete if it belongs to the user
284+
cur.execute("DELETE FROM qd_analysis_memory WHERE id = %s AND user_id = %s", (memory_id, user_id))
285+
else:
286+
cur.execute("DELETE FROM qd_analysis_memory WHERE id = %s", (memory_id,))
271287
db.commit()
272288
affected = cur.rowcount
273289
cur.close()

backend_api_python/app/services/fast_analysis.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -442,10 +442,18 @@ def _format_macro_summary(self, macro: Dict[str, Any], market: str) -> str:
442442
# ==================== Main Analysis ====================
443443

444444
def analyze(self, market: str, symbol: str, language: str = 'en-US',
445-
model: str = None, timeframe: str = "1D") -> Dict[str, Any]:
445+
model: str = None, timeframe: str = "1D", user_id: int = None) -> Dict[str, Any]:
446446
"""
447447
Run fast single-call analysis.
448448
449+
Args:
450+
market: Market type (Crypto, USStock, etc.)
451+
symbol: Trading pair or stock symbol
452+
language: Response language (zh-CN or en-US)
453+
model: LLM model to use
454+
timeframe: Analysis timeframe (1D, 4H, etc.)
455+
user_id: User ID for storing analysis history
456+
449457
Returns:
450458
Complete analysis result with actionable recommendations.
451459
"""
@@ -587,11 +595,11 @@ def analyze(self, market: str, symbol: str, language: str = 'en-US',
587595
})
588596

589597
# Store in memory for future retrieval and get memory_id for feedback
590-
memory_id = self._store_analysis_memory(result)
598+
memory_id = self._store_analysis_memory(result, user_id=user_id)
591599
if memory_id:
592600
result["memory_id"] = memory_id
593601

594-
logger.info(f"Fast analysis completed in {total_time}ms: {market}:{symbol} -> {result['decision']} (memory_id={memory_id})")
602+
logger.info(f"Fast analysis completed in {total_time}ms: {market}:{symbol} -> {result['decision']} (memory_id={memory_id}, user_id={user_id})")
595603

596604
except Exception as e:
597605
logger.error(f"Fast analysis failed: {e}", exc_info=True)
@@ -665,12 +673,12 @@ def _calculate_overall_score(self, analysis: Dict) -> int:
665673

666674
return max(0, min(100, int(overall)))
667675

668-
def _store_analysis_memory(self, result: Dict) -> Optional[int]:
676+
def _store_analysis_memory(self, result: Dict, user_id: int = None) -> Optional[int]:
669677
"""Store analysis result for future learning. Returns memory_id."""
670678
try:
671679
from app.services.analysis_memory import get_analysis_memory
672680
memory = get_analysis_memory()
673-
memory_id = memory.store(result)
681+
memory_id = memory.store(result, user_id=user_id)
674682
return memory_id
675683
except Exception as e:
676684
logger.warning(f"Memory storage failed: {e}")
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
-- Migration: Add user_id column to qd_analysis_memory table
2+
-- This allows filtering analysis history by user
3+
-- Run this migration to update existing databases
4+
5+
-- Add user_id column if it doesn't exist
6+
DO $$
7+
BEGIN
8+
IF NOT EXISTS (
9+
SELECT 1 FROM information_schema.columns
10+
WHERE table_name = 'qd_analysis_memory' AND column_name = 'user_id'
11+
) THEN
12+
ALTER TABLE qd_analysis_memory ADD COLUMN user_id INT;
13+
14+
-- Create index for efficient user-based queries
15+
CREATE INDEX IF NOT EXISTS idx_analysis_memory_user ON qd_analysis_memory(user_id);
16+
17+
RAISE NOTICE 'Added user_id column to qd_analysis_memory';
18+
ELSE
19+
RAISE NOTICE 'user_id column already exists in qd_analysis_memory';
20+
END IF;
21+
END $$;

docs/CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ This document records version updates, new features, bug fixes, and database mig
3939
- Fixed A-share and H-share data fetching with multiple fallback sources
4040
- Fixed watchlist price batch fetch timeout handling
4141
- Fixed heatmap multi-language support for commodities and forex
42+
- **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
43+
- **Fixed "Missing Turnstile token" error when changing password** - Logged-in users no longer need Turnstile verification to request password change verification code
4244

4345
### 🎨 UI/UX Improvements
4446
- Reorganized left menu: Indicator Market moved below Indicator Analysis, Settings moved to bottom
@@ -92,9 +94,21 @@ BEGIN
9294
END IF;
9395
END $$;
9496

97+
-- Add user_id column for user-specific history filtering
98+
DO $$
99+
BEGIN
100+
IF NOT EXISTS (
101+
SELECT 1 FROM information_schema.columns
102+
WHERE table_name = 'qd_analysis_memory' AND column_name = 'user_id'
103+
) THEN
104+
ALTER TABLE qd_analysis_memory ADD COLUMN user_id INT;
105+
END IF;
106+
END $$;
107+
95108
CREATE INDEX IF NOT EXISTS idx_analysis_memory_symbol ON qd_analysis_memory(market, symbol);
96109
CREATE INDEX IF NOT EXISTS idx_analysis_memory_created ON qd_analysis_memory(created_at DESC);
97110
CREATE INDEX IF NOT EXISTS idx_analysis_memory_validated ON qd_analysis_memory(validated_at) WHERE validated_at IS NOT NULL;
111+
CREATE INDEX IF NOT EXISTS idx_analysis_memory_user ON qd_analysis_memory(user_id);
98112

99113
-- 2. Indicator Purchase Records
100114
CREATE TABLE IF NOT EXISTS qd_indicator_purchases (

0 commit comments

Comments
 (0)