Harshilforworks commited on
Commit
76be78d
Β·
verified Β·
1 Parent(s): 6e6d287

Upload 7 files

Browse files
Files changed (2) hide show
  1. create_test_users.py +99 -0
  2. main.py +171 -13
create_test_users.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Script to create test users for Plus and Pro tiers.
3
+ Run with: python3 create_test_users.py
4
+ """
5
+ import os
6
+ import sys
7
+ from datetime import datetime
8
+ import hashlib
9
+ from dotenv import load_dotenv
10
+
11
+ load_dotenv()
12
+
13
+ # Add parent directory to path to import MongoDBService
14
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
15
+
16
+ from services.mongodb_service import MongoDBService
17
+
18
+ def hash_password(password: str) -> str:
19
+ """Hash password using SHA256 (matches backend login logic)"""
20
+ return hashlib.sha256(password.encode()).hexdigest()
21
+
22
+ def create_test_users():
23
+ """Create Plus and Pro test users"""
24
+ try:
25
+ mongodb = MongoDBService()
26
+
27
+ # Test user credentials
28
+ test_users = [
29
+ {
30
+ "email": "[email protected]",
31
+ "plain_password": "PlusTester!123",
32
+ "password": hash_password("PlusTester!123"),
33
+ "name": "Plus Test User",
34
+ "subscription_tier": "Pro", # Maps to Plus tier
35
+ "domain_preferences": ["Technology", "Science"],
36
+ "phone_number": "+1234567890",
37
+ "age": 25,
38
+ },
39
+ {
40
+ "email": "[email protected]",
41
+ "plain_password": "ProTester!123",
42
+ "password": hash_password("ProTester!123"),
43
+ "name": "Pro Test User",
44
+ "subscription_tier": "Enterprise", # Maps to Pro tier
45
+ "domain_preferences": ["Technology", "Science", "Politics", "Health"],
46
+ "phone_number": "+1234567891",
47
+ "age": 30,
48
+ }
49
+ ]
50
+
51
+ print("πŸ” Creating test users...")
52
+
53
+ for user_data in test_users:
54
+ email = user_data["email"]
55
+
56
+ # Check if user already exists
57
+ existing = mongodb.users.find_one({"email": email})
58
+ if existing:
59
+ print(f"⚠️ User {email} already exists. Updating subscription tier...")
60
+ mongodb.update_user_subscription_tier(
61
+ str(existing["_id"]),
62
+ user_data["subscription_tier"]
63
+ )
64
+ print(f"βœ… Updated {email} to {user_data['subscription_tier']} tier")
65
+ else:
66
+ # Create new user - remove plain_password before inserting
67
+ user_insert_data = {k: v for k, v in user_data.items() if k != "plain_password"}
68
+ user_insert_data["created_at"] = datetime.utcnow()
69
+ user_insert_data["updated_at"] = datetime.utcnow()
70
+
71
+ result = mongodb.users.insert_one(user_insert_data)
72
+ user_data["_id"] = str(result.inserted_id)
73
+ user_data["id"] = str(result.inserted_id)
74
+
75
+ print(f"βœ… Created {email} with tier: {user_data['subscription_tier']}")
76
+
77
+ print("\nβœ… Test users created/updated successfully!")
78
+ print("\nπŸ“‹ Login credentials:")
79
+ print("=" * 60)
80
+ for user_data in test_users:
81
+ print(f"\nEmail: {user_data['email']}")
82
+ print(f"Password: {user_data['plain_password']}")
83
+ if user_data['subscription_tier'] == "Pro":
84
+ print("Tier: Plus (subscription_tier: Pro)")
85
+ elif user_data['subscription_tier'] == "Enterprise":
86
+ print("Tier: Pro (subscription_tier: Enterprise)")
87
+ print("=" * 60)
88
+
89
+ mongodb.close()
90
+
91
+ except Exception as e:
92
+ print(f"❌ Error creating test users: {e}")
93
+ import traceback
94
+ traceback.print_exc()
95
+ sys.exit(1)
96
+
97
+ if __name__ == "__main__":
98
+ create_test_users()
99
+
main.py CHANGED
@@ -1,6 +1,6 @@
1
  from fastapi import FastAPI, File, UploadFile, HTTPException, Form, WebSocket, WebSocketDisconnect, Request
2
  from typing import Optional, List, Dict, Any
3
- from fastapi.responses import FileResponse
4
  from fastapi.middleware.cors import CORSMiddleware
5
  from fastapi.staticfiles import StaticFiles
6
  import uvicorn
@@ -64,6 +64,64 @@ app.mount("/static", StaticFiles(directory="public"), name="static")
64
  app.mount("/frames", StaticFiles(directory="public/frames"), name="frames")
65
 
66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  # Initialize verifiers and input processor
68
  image_verifier = ImageVerifier()
69
  video_verifier = VideoVerifier()
@@ -770,8 +828,11 @@ async def _verify_youtube_video(url: str, claim_context: str, claim_date: str) -
770
 
771
  @app.post("/chatbot/verify")
772
  async def chatbot_verify(
 
773
  text_input: Optional[str] = Form(None),
774
- files: Optional[List[UploadFile]] = File(None)
 
 
775
  ):
776
  """
777
  Chatbot-friendly endpoint that intelligently processes input and routes to appropriate verification
@@ -781,6 +842,60 @@ async def chatbot_verify(
781
  print(f"πŸ” DEBUG: text_input = {text_input}")
782
  print(f"πŸ” DEBUG: files = {files}")
783
  print(f"πŸ” DEBUG: files type = {type(files)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
784
  received_files_meta: List[Dict[str, Any]] = []
785
  if files:
786
  for i, file in enumerate(files):
@@ -1298,28 +1413,61 @@ async def speech_to_text(
1298
  raise HTTPException(status_code=500, detail=str(e))
1299
 
1300
 
1301
- # Educational Content API Endpoints
1302
  @app.get("/educational/modules")
1303
  async def get_educational_modules():
1304
- """Get list of available educational modules"""
1305
  try:
1306
- modules_data = await educational_generator.get_modules_list()
1307
- return modules_data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1308
  except Exception as e:
 
1309
  raise HTTPException(status_code=500, detail=str(e))
1310
 
1311
  @app.get("/educational/modules/{module_id}")
1312
  async def get_module_content(
1313
  module_id: str,
1314
- difficulty_level: str = "beginner"
1315
  ):
1316
- """Get educational content for a specific module"""
1317
  try:
1318
- content = await educational_generator.generate_module_content(
1319
- module_id, difficulty_level
 
 
 
 
 
 
 
 
 
 
 
 
 
1320
  )
1321
- return content
 
1322
  except Exception as e:
 
1323
  raise HTTPException(status_code=500, detail=str(e))
1324
 
1325
  @app.post("/educational/contextual-learning")
@@ -1335,14 +1483,24 @@ async def get_contextual_learning(verification_result: Dict[str, Any]):
1335
 
1336
  @app.post("/educational/clear-cache")
1337
  async def clear_educational_cache():
1338
- """Clear all educational content from Redis cache"""
 
 
 
 
 
 
1339
  try:
1340
  if educational_generator.redis_client:
1341
  # Get all educational cache keys
1342
  keys = educational_generator.redis_client.keys("educational:*")
1343
  if keys:
1344
  educational_generator.redis_client.delete(*keys)
1345
- return {"message": f"Cleared {len(keys)} cache entries", "keys": keys}
 
 
 
 
1346
  else:
1347
  return {"message": "No cache entries found"}
1348
  else:
 
1
  from fastapi import FastAPI, File, UploadFile, HTTPException, Form, WebSocket, WebSocketDisconnect, Request
2
  from typing import Optional, List, Dict, Any
3
+ from fastapi.responses import FileResponse, JSONResponse
4
  from fastapi.middleware.cors import CORSMiddleware
5
  from fastapi.staticfiles import StaticFiles
6
  import uvicorn
 
64
  app.mount("/frames", StaticFiles(directory="public/frames"), name="frames")
65
 
66
 
67
+ # ---------- Tier configuration ----------
68
+
69
+ # Public-facing tiers used across the product
70
+ NORMALIZED_TIERS = ("Free", "Plus", "Pro")
71
+
72
+ # Map stored subscription_tier / plan_name values to normalized tiers.
73
+ # This keeps backward compatibility with any existing users whose tier
74
+ # might still be stored as \"Pro\" or \"Enterprise\".
75
+ SUBSCRIPTION_TIER_MAPPING = {
76
+ "free": "Free",
77
+ "plus": "Plus",
78
+ "pro": "Plus", # legacy Pro maps to Plus
79
+ "enterprise": "Pro", # highest tier maps to Pro
80
+ }
81
+
82
+ # Central limits per tier so they can be tuned in one place.
83
+ # These values are intentionally conservative to protect API costs.
84
+ TIER_LIMITS = {
85
+ "Free": {
86
+ "daily_verifications": 5,
87
+ "monthly_verifications": 25,
88
+ "max_chat_sessions": 1,
89
+ "max_messages_per_session": 10,
90
+ },
91
+ "Plus": {
92
+ "daily_verifications": 10,
93
+ "monthly_verifications": 50,
94
+ "max_chat_sessions": 5,
95
+ "max_messages_per_session": 50,
96
+ },
97
+ "Pro": {
98
+ "daily_verifications": 25,
99
+ "monthly_verifications": 200,
100
+ "max_chat_sessions": 20,
101
+ "max_messages_per_session": 200,
102
+ },
103
+ }
104
+
105
+
106
+ def get_normalized_tier(raw_tier: str | None) -> str:
107
+ """
108
+ Normalize any stored subscription_tier / plan_name to one of
109
+ the public-facing tiers: Free, Plus, Pro.
110
+ """
111
+ if not raw_tier:
112
+ return "Free"
113
+ key = str(raw_tier).strip().lower()
114
+ return SUBSCRIPTION_TIER_MAPPING.get(key, "Free")
115
+
116
+
117
+ def get_tier_limits(raw_tier: str | None) -> dict:
118
+ """
119
+ Return the limits dict for a given stored tier value.
120
+ """
121
+ normalized = get_normalized_tier(raw_tier)
122
+ return TIER_LIMITS.get(normalized, TIER_LIMITS["Free"])
123
+
124
+
125
  # Initialize verifiers and input processor
126
  image_verifier = ImageVerifier()
127
  video_verifier = VideoVerifier()
 
828
 
829
  @app.post("/chatbot/verify")
830
  async def chatbot_verify(
831
+ request: Request,
832
  text_input: Optional[str] = Form(None),
833
+ files: Optional[List[UploadFile]] = File(None),
834
+ anonymous_id: Optional[str] = Form(None),
835
+ user_id: Optional[str] = Form(None),
836
  ):
837
  """
838
  Chatbot-friendly endpoint that intelligently processes input and routes to appropriate verification
 
842
  print(f"πŸ” DEBUG: text_input = {text_input}")
843
  print(f"πŸ” DEBUG: files = {files}")
844
  print(f"πŸ” DEBUG: files type = {type(files)}")
845
+ print(f"πŸ” DEBUG: anonymous_id = {anonymous_id}")
846
+ print(f"πŸ” DEBUG: user_id = {user_id}")
847
+
848
+ # Determine logical user key and tier for rate limiting
849
+ user_doc = None
850
+ raw_tier = "Free"
851
+ if user_id and mongodb_service:
852
+ try:
853
+ user_doc = mongodb_service.get_user_by_id(user_id)
854
+ except Exception as e:
855
+ logger.warning(
856
+ f"⚠️ Failed to load user {user_id} for tier resolution: {e}"
857
+ )
858
+
859
+ if user_doc:
860
+ raw_tier = user_doc.get("subscription_tier") or "Free"
861
+ else:
862
+ raw_tier = "Free"
863
+
864
+ limits = get_tier_limits(raw_tier)
865
+ key_host = getattr(request.client, "host", "unknown")
866
+ key = user_id or anonymous_id or f"ip:{key_host}"
867
+
868
+ if mongodb_service:
869
+ usage_info = mongodb_service.increment_usage_and_check_limits(
870
+ key=key,
871
+ feature="verification",
872
+ daily_limit=limits.get("daily_verifications"),
873
+ monthly_limit=limits.get("monthly_verifications"),
874
+ )
875
+ else:
876
+ usage_info = {
877
+ "allowed": True,
878
+ "tier_limits": {
879
+ "daily": limits.get("daily_verifications"),
880
+ "monthly": limits.get("monthly_verifications"),
881
+ },
882
+ }
883
+
884
+ if not usage_info.get("allowed", True):
885
+ normalized_tier = get_normalized_tier(raw_tier)
886
+ return JSONResponse(
887
+ status_code=429,
888
+ content={
889
+ "error": "verification_limit_reached",
890
+ "tier": normalized_tier,
891
+ "key": key,
892
+ "limits": usage_info.get("tier_limits"),
893
+ "usage": {
894
+ "daily": usage_info.get("daily"),
895
+ "monthly": usage_info.get("monthly"),
896
+ },
897
+ },
898
+ )
899
  received_files_meta: List[Dict[str, Any]] = []
900
  if files:
901
  for i, file in enumerate(files):
 
1413
  raise HTTPException(status_code=500, detail=str(e))
1414
 
1415
 
1416
+ # Educational Content API Endpoints - Now fetching from MongoDB weekly_posts
1417
  @app.get("/educational/modules")
1418
  async def get_educational_modules():
1419
+ """Get list of available educational modules from MongoDB weekly_posts"""
1420
  try:
1421
+ if not mongodb_service:
1422
+ raise HTTPException(status_code=503, detail="MongoDB service not available")
1423
+
1424
+ modules_list = mongodb_service.get_educational_modules_list()
1425
+ response_data = {
1426
+ "modules": modules_list,
1427
+ "total": len(modules_list)
1428
+ }
1429
+ # Return with no-cache headers to prevent stale cache in production
1430
+ return JSONResponse(
1431
+ content=response_data,
1432
+ headers={
1433
+ "Cache-Control": "no-cache, no-store, must-revalidate, max-age=0",
1434
+ "Pragma": "no-cache",
1435
+ "Expires": "0"
1436
+ }
1437
+ )
1438
+ except HTTPException:
1439
+ raise
1440
  except Exception as e:
1441
+ logger.error(f"Failed to get educational modules: {e}")
1442
  raise HTTPException(status_code=500, detail=str(e))
1443
 
1444
  @app.get("/educational/modules/{module_id}")
1445
  async def get_module_content(
1446
  module_id: str,
1447
+ difficulty_level: str = "beginner" # Kept for backward compatibility but not used
1448
  ):
1449
+ """Get educational content for a specific module from MongoDB weekly_posts"""
1450
  try:
1451
+ if not mongodb_service:
1452
+ raise HTTPException(status_code=503, detail="MongoDB service not available")
1453
+
1454
+ content = mongodb_service.get_educational_module_by_id(module_id)
1455
+ if not content:
1456
+ raise HTTPException(status_code=404, detail=f"Module '{module_id}' not found")
1457
+
1458
+ # Return with no-cache headers to prevent stale cache in production
1459
+ return JSONResponse(
1460
+ content=content,
1461
+ headers={
1462
+ "Cache-Control": "no-cache, no-store, must-revalidate, max-age=0",
1463
+ "Pragma": "no-cache",
1464
+ "Expires": "0"
1465
+ }
1466
  )
1467
+ except HTTPException:
1468
+ raise
1469
  except Exception as e:
1470
+ logger.error(f"Failed to get module content: {e}")
1471
  raise HTTPException(status_code=500, detail=str(e))
1472
 
1473
  @app.post("/educational/contextual-learning")
 
1483
 
1484
  @app.post("/educational/clear-cache")
1485
  async def clear_educational_cache():
1486
+ """
1487
+ Clear all educational content from Redis cache.
1488
+
1489
+ Note: The /educational/modules endpoints now use no-cache headers
1490
+ to prevent browser/CDN caching. This endpoint is mainly for clearing
1491
+ any legacy Redis cache entries.
1492
+ """
1493
  try:
1494
  if educational_generator.redis_client:
1495
  # Get all educational cache keys
1496
  keys = educational_generator.redis_client.keys("educational:*")
1497
  if keys:
1498
  educational_generator.redis_client.delete(*keys)
1499
+ return {
1500
+ "message": f"Cleared {len(keys)} cache entries",
1501
+ "keys": keys,
1502
+ "note": "Educational endpoints use no-cache headers to prevent stale data"
1503
+ }
1504
  else:
1505
  return {"message": "No cache entries found"}
1506
  else: