Spaces:
Sleeping
Sleeping
Commit
·
356ac4f
1
Parent(s):
f20194e
Updated files
Browse files- Dockerfile +1 -1
- app.py +784 -0
- data/active_sessions.json +3 -3
- main.py +0 -234
- requirements.txt +1 -1
- src/components/__pycache__/auth.cpython-311.pyc +0 -0
- src/components/__pycache__/config.cpython-311.pyc +0 -0
- src/components/auth.py +139 -194
- src/scripts/dev_server.py +2 -2
- src/static/js/main.js +214 -69
- src/templates/login.html +84 -174
Dockerfile
CHANGED
|
@@ -108,4 +108,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
|
| 108 |
CMD curl -f http://localhost:7860/health || exit 1
|
| 109 |
|
| 110 |
# Start the application
|
| 111 |
-
CMD ["python", "
|
|
|
|
| 108 |
CMD curl -f http://localhost:7860/health || exit 1
|
| 109 |
|
| 110 |
# Start the application
|
| 111 |
+
CMD ["python", "app.py"]
|
app.py
ADDED
|
@@ -0,0 +1,784 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
# ==== Hugging Face Spaces Environment Setup (from main.py) ====
|
| 3 |
+
import os
|
| 4 |
+
import sys
|
| 5 |
+
import json
|
| 6 |
+
import asyncio
|
| 7 |
+
from typing import Dict, List, Optional, Any
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from contextlib import asynccontextmanager
|
| 11 |
+
|
| 12 |
+
# Set up environment variables for Hugging Face Spaces compatibility
|
| 13 |
+
def setup_environment():
|
| 14 |
+
env_vars = {
|
| 15 |
+
'DATA_DIR': '/tmp/researchmate/data',
|
| 16 |
+
'LOGS_DIR': '/tmp/researchmate/logs',
|
| 17 |
+
'CHROMA_DIR': '/tmp/researchmate/chroma_persist',
|
| 18 |
+
'UPLOADS_DIR': '/tmp/researchmate/uploads',
|
| 19 |
+
'CHROMA_DB_DIR': '/tmp/researchmate/chroma_db',
|
| 20 |
+
'CONFIG_DIR': '/tmp/researchmate/config',
|
| 21 |
+
'TEMP_DIR': '/tmp/researchmate/tmp',
|
| 22 |
+
'CHROMA_PERSIST_DIR': '/tmp/researchmate/chroma_persist',
|
| 23 |
+
'MPLCONFIGDIR': '/tmp/matplotlib',
|
| 24 |
+
'TRANSFORMERS_CACHE': '/tmp/transformers',
|
| 25 |
+
'HF_HOME': '/tmp/huggingface',
|
| 26 |
+
'SENTENCE_TRANSFORMERS_HOME': '/tmp/sentence_transformers',
|
| 27 |
+
'HF_DATASETS_CACHE': '/tmp/datasets',
|
| 28 |
+
'HUGGINGFACE_HUB_CACHE': '/tmp/huggingface_hub',
|
| 29 |
+
'XDG_CACHE_HOME': '/tmp/cache',
|
| 30 |
+
'PYTORCH_KERNEL_CACHE_PATH': '/tmp/cache',
|
| 31 |
+
'TORCH_HOME': '/tmp/cache',
|
| 32 |
+
'NLTK_DATA': '/tmp/cache/nltk_data',
|
| 33 |
+
'TOKENIZERS_PARALLELISM': 'false',
|
| 34 |
+
'HOME': '/tmp/cache',
|
| 35 |
+
'TMPDIR': '/tmp/researchmate/tmp',
|
| 36 |
+
'HF_DATASETS_OFFLINE': '1',
|
| 37 |
+
'HF_HUB_OFFLINE': '0',
|
| 38 |
+
}
|
| 39 |
+
for key, value in env_vars.items():
|
| 40 |
+
os.environ[key] = value
|
| 41 |
+
sys.path.insert(0, '/tmp/cache')
|
| 42 |
+
directories = [
|
| 43 |
+
'/tmp/researchmate/data',
|
| 44 |
+
'/tmp/researchmate/logs',
|
| 45 |
+
'/tmp/researchmate/chroma_persist',
|
| 46 |
+
'/tmp/researchmate/uploads',
|
| 47 |
+
'/tmp/researchmate/chroma_db',
|
| 48 |
+
'/tmp/researchmate/config',
|
| 49 |
+
'/tmp/researchmate/tmp',
|
| 50 |
+
'/tmp/matplotlib',
|
| 51 |
+
'/tmp/transformers',
|
| 52 |
+
'/tmp/huggingface',
|
| 53 |
+
'/tmp/sentence_transformers',
|
| 54 |
+
'/tmp/datasets',
|
| 55 |
+
'/tmp/huggingface_hub',
|
| 56 |
+
'/tmp/cache',
|
| 57 |
+
'/tmp/cache/nltk_data'
|
| 58 |
+
]
|
| 59 |
+
for directory in directories:
|
| 60 |
+
try:
|
| 61 |
+
path = Path(directory)
|
| 62 |
+
path.mkdir(parents=True, exist_ok=True)
|
| 63 |
+
path.chmod(0o777)
|
| 64 |
+
except Exception as e:
|
| 65 |
+
print(f"⚠ Warning: Could not create directory {directory}: {e}")
|
| 66 |
+
|
| 67 |
+
setup_environment()
|
| 68 |
+
|
| 69 |
+
# Add the project root to Python path
|
| 70 |
+
sys.path.append(str(Path(__file__).parent))
|
| 71 |
+
|
| 72 |
+
from fastapi import FastAPI, HTTPException, UploadFile, File, Form, Request, Depends
|
| 73 |
+
from fastapi.staticfiles import StaticFiles
|
| 74 |
+
from fastapi.templating import Jinja2Templates
|
| 75 |
+
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse, FileResponse
|
| 76 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 77 |
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
| 78 |
+
from pydantic import BaseModel, Field
|
| 79 |
+
import uvicorn
|
| 80 |
+
|
| 81 |
+
# Import settings and ResearchMate components
|
| 82 |
+
from src.components.research_assistant import ResearchMate
|
| 83 |
+
from src.components.citation_network import CitationNetworkAnalyzer
|
| 84 |
+
from src.components.auth import AuthManager
|
| 85 |
+
|
| 86 |
+
# Initialize only essential components at startup (fast components only)
|
| 87 |
+
auth_manager = AuthManager()
|
| 88 |
+
security = HTTPBearer(auto_error=False)
|
| 89 |
+
|
| 90 |
+
# Simple settings for development
|
| 91 |
+
|
| 92 |
+
# Settings for Hugging Face Spaces: use /tmp dirs
|
| 93 |
+
class Settings:
|
| 94 |
+
def __init__(self):
|
| 95 |
+
self.server = type('ServerSettings', (), {
|
| 96 |
+
'debug': False,
|
| 97 |
+
'host': '0.0.0.0',
|
| 98 |
+
'port': int(os.environ.get('PORT', 7860)) # Default to 7860 for HF Spaces
|
| 99 |
+
})()
|
| 100 |
+
self.security = type('SecuritySettings', (), {
|
| 101 |
+
'cors_origins': ["*"],
|
| 102 |
+
'cors_methods': ["*"],
|
| 103 |
+
'cors_headers': ["*"]
|
| 104 |
+
})()
|
| 105 |
+
def get_static_dir(self):
|
| 106 |
+
return "/tmp/researchmate/static"
|
| 107 |
+
def get_templates_dir(self):
|
| 108 |
+
return "src/templates" # Templates can remain in src
|
| 109 |
+
def get_upload_dir(self):
|
| 110 |
+
return "/tmp/researchmate/uploads"
|
| 111 |
+
def get_logs_dir(self):
|
| 112 |
+
return "/tmp/researchmate/logs"
|
| 113 |
+
|
| 114 |
+
settings = Settings()
|
| 115 |
+
|
| 116 |
+
# Initialize ResearchMate and Citation Analyzer (will be done during loading screen)
|
| 117 |
+
research_mate = None
|
| 118 |
+
citation_analyzer = None
|
| 119 |
+
|
| 120 |
+
# Global initialization flag
|
| 121 |
+
research_mate_initialized = False
|
| 122 |
+
initialization_in_progress = False
|
| 123 |
+
|
| 124 |
+
async def initialize_research_mate():
|
| 125 |
+
"""Initialize ResearchMate and Citation Analyzer in the background"""
|
| 126 |
+
global research_mate, citation_analyzer, research_mate_initialized, initialization_in_progress
|
| 127 |
+
|
| 128 |
+
if initialization_in_progress:
|
| 129 |
+
return
|
| 130 |
+
|
| 131 |
+
initialization_in_progress = True
|
| 132 |
+
print("🚀 Starting ResearchMate background initialization...")
|
| 133 |
+
|
| 134 |
+
try:
|
| 135 |
+
# Run initialization in thread pool to avoid blocking
|
| 136 |
+
import concurrent.futures
|
| 137 |
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
| 138 |
+
loop = asyncio.get_event_loop()
|
| 139 |
+
|
| 140 |
+
print("📊 Initializing Citation Network Analyzer...")
|
| 141 |
+
citation_analyzer = await loop.run_in_executor(executor, CitationNetworkAnalyzer)
|
| 142 |
+
print("✅ Citation Network Analyzer initialized!")
|
| 143 |
+
|
| 144 |
+
print("🧠 Initializing ResearchMate core...")
|
| 145 |
+
research_mate = await loop.run_in_executor(executor, ResearchMate)
|
| 146 |
+
print("✅ ResearchMate core initialized!")
|
| 147 |
+
|
| 148 |
+
research_mate_initialized = True
|
| 149 |
+
print("🎉 All components initialized successfully!")
|
| 150 |
+
except Exception as e:
|
| 151 |
+
print(f"❌ Failed to initialize components: {e}")
|
| 152 |
+
print("⚠️ Server will start but some features may not work")
|
| 153 |
+
research_mate = None
|
| 154 |
+
citation_analyzer = None
|
| 155 |
+
research_mate_initialized = False
|
| 156 |
+
finally:
|
| 157 |
+
initialization_in_progress = False
|
| 158 |
+
|
| 159 |
+
# Pydantic models for API
|
| 160 |
+
class SearchQuery(BaseModel):
|
| 161 |
+
query: str = Field(..., description="Search query")
|
| 162 |
+
max_results: int = Field(default=10, ge=1, le=50, description="Maximum number of results")
|
| 163 |
+
|
| 164 |
+
class QuestionQuery(BaseModel):
|
| 165 |
+
question: str = Field(..., description="Research question")
|
| 166 |
+
|
| 167 |
+
class ProjectCreate(BaseModel):
|
| 168 |
+
name: str = Field(..., description="Project name")
|
| 169 |
+
research_question: str = Field(..., description="Research question")
|
| 170 |
+
keywords: List[str] = Field(..., description="Keywords")
|
| 171 |
+
|
| 172 |
+
class ProjectQuery(BaseModel):
|
| 173 |
+
project_id: str = Field(..., description="Project ID")
|
| 174 |
+
question: str = Field(..., description="Question about the project")
|
| 175 |
+
|
| 176 |
+
class TrendQuery(BaseModel):
|
| 177 |
+
topic: str = Field(..., description="Research topic")
|
| 178 |
+
|
| 179 |
+
# Authentication models
|
| 180 |
+
class LoginRequest(BaseModel):
|
| 181 |
+
username: str = Field(..., description="Username")
|
| 182 |
+
password: str = Field(..., description="Password")
|
| 183 |
+
|
| 184 |
+
class RegisterRequest(BaseModel):
|
| 185 |
+
username: str = Field(..., description="Username")
|
| 186 |
+
email: str = Field(..., description="Email address")
|
| 187 |
+
password: str = Field(..., description="Password")
|
| 188 |
+
|
| 189 |
+
# Authentication dependency for API endpoints
|
| 190 |
+
async def get_current_user_dependency(request: Request, credentials: HTTPAuthorizationCredentials = Depends(security)):
|
| 191 |
+
user = None
|
| 192 |
+
|
| 193 |
+
# Try Authorization header first
|
| 194 |
+
if credentials:
|
| 195 |
+
user = auth_manager.verify_token(credentials.credentials)
|
| 196 |
+
|
| 197 |
+
# If no user from header, try cookie
|
| 198 |
+
if not user:
|
| 199 |
+
token = request.cookies.get('authToken')
|
| 200 |
+
if token:
|
| 201 |
+
user = auth_manager.verify_token(token)
|
| 202 |
+
|
| 203 |
+
if not user:
|
| 204 |
+
raise HTTPException(status_code=401, detail="Authentication required")
|
| 205 |
+
|
| 206 |
+
return user
|
| 207 |
+
|
| 208 |
+
# Authentication for web pages (checks both header and cookie)
|
| 209 |
+
async def get_current_user_web(request: Request):
|
| 210 |
+
"""Get current user for web page requests (checks both Authorization header and cookies)"""
|
| 211 |
+
user = None
|
| 212 |
+
|
| 213 |
+
# First try Authorization header
|
| 214 |
+
try:
|
| 215 |
+
credentials = await security(request)
|
| 216 |
+
if credentials:
|
| 217 |
+
user = auth_manager.verify_token(credentials.credentials)
|
| 218 |
+
except:
|
| 219 |
+
pass
|
| 220 |
+
|
| 221 |
+
# If no user from header, try cookie
|
| 222 |
+
if not user:
|
| 223 |
+
token = request.cookies.get('authToken')
|
| 224 |
+
if token:
|
| 225 |
+
user = auth_manager.verify_token(token)
|
| 226 |
+
|
| 227 |
+
return user
|
| 228 |
+
|
| 229 |
+
# Background task to clean up expired sessions
|
| 230 |
+
async def cleanup_expired_sessions():
|
| 231 |
+
while True:
|
| 232 |
+
try:
|
| 233 |
+
expired_count = auth_manager.cleanup_expired_sessions()
|
| 234 |
+
if expired_count > 0:
|
| 235 |
+
print(f"Cleaned up {expired_count} expired sessions")
|
| 236 |
+
except Exception as e:
|
| 237 |
+
print(f"Error cleaning up sessions: {e}")
|
| 238 |
+
|
| 239 |
+
# Run cleanup every 30 minutes
|
| 240 |
+
await asyncio.sleep(30 * 60)
|
| 241 |
+
|
| 242 |
+
@asynccontextmanager
|
| 243 |
+
async def lifespan(app: FastAPI):
|
| 244 |
+
# Start ResearchMate initialization in background (non-blocking)
|
| 245 |
+
asyncio.create_task(initialize_research_mate())
|
| 246 |
+
|
| 247 |
+
# Start background cleanup task
|
| 248 |
+
cleanup_task = asyncio.create_task(cleanup_expired_sessions())
|
| 249 |
+
|
| 250 |
+
try:
|
| 251 |
+
yield
|
| 252 |
+
finally:
|
| 253 |
+
cleanup_task.cancel()
|
| 254 |
+
try:
|
| 255 |
+
await cleanup_task
|
| 256 |
+
except asyncio.CancelledError:
|
| 257 |
+
pass
|
| 258 |
+
|
| 259 |
+
# Initialize FastAPI app with lifespan
|
| 260 |
+
app = FastAPI(
|
| 261 |
+
title="ResearchMate API",
|
| 262 |
+
description="AI Research Assistant powered by Groq Llama 3.3 70B",
|
| 263 |
+
version="1.0.0",
|
| 264 |
+
debug=settings.server.debug,
|
| 265 |
+
lifespan=lifespan
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
# Add CORS middleware
|
| 269 |
+
app.add_middleware(
|
| 270 |
+
CORSMiddleware,
|
| 271 |
+
allow_origins=settings.security.cors_origins,
|
| 272 |
+
allow_credentials=True,
|
| 273 |
+
allow_methods=settings.security.cors_methods,
|
| 274 |
+
allow_headers=settings.security.cors_headers,
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
# Mount static files with cache control for development
|
| 278 |
+
static_dir = Path(settings.get_static_dir())
|
| 279 |
+
static_dir.mkdir(parents=True, exist_ok=True)
|
| 280 |
+
|
| 281 |
+
# Custom static files class to add no-cache headers for development
|
| 282 |
+
class NoCacheStaticFiles(StaticFiles):
|
| 283 |
+
def file_response(self, full_path, stat_result, scope):
|
| 284 |
+
response = FileResponse(
|
| 285 |
+
path=full_path,
|
| 286 |
+
stat_result=stat_result
|
| 287 |
+
)
|
| 288 |
+
# Add no-cache headers for development
|
| 289 |
+
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
| 290 |
+
response.headers["Pragma"] = "no-cache"
|
| 291 |
+
response.headers["Expires"] = "0"
|
| 292 |
+
return response
|
| 293 |
+
|
| 294 |
+
app.mount("/static", NoCacheStaticFiles(directory=str(static_dir)), name="static")
|
| 295 |
+
|
| 296 |
+
# Templates
|
| 297 |
+
templates_dir = Path(settings.get_templates_dir())
|
| 298 |
+
templates_dir.mkdir(parents=True, exist_ok=True)
|
| 299 |
+
templates = Jinja2Templates(directory=str(templates_dir))
|
| 300 |
+
|
| 301 |
+
# Loading page route
|
| 302 |
+
@app.get("/loading", response_class=HTMLResponse)
|
| 303 |
+
async def loading_page(request: Request):
|
| 304 |
+
return templates.TemplateResponse("loading.html", {"request": request})
|
| 305 |
+
|
| 306 |
+
# Authentication routes
|
| 307 |
+
@app.post("/api/auth/register")
|
| 308 |
+
async def register(request: RegisterRequest):
|
| 309 |
+
result = auth_manager.create_user(request.username, request.email, request.password)
|
| 310 |
+
if result["success"]:
|
| 311 |
+
return {"success": True, "message": "Account created successfully"}
|
| 312 |
+
else:
|
| 313 |
+
raise HTTPException(status_code=400, detail=result["error"])
|
| 314 |
+
|
| 315 |
+
@app.post("/api/auth/login")
|
| 316 |
+
async def login(request: LoginRequest):
|
| 317 |
+
result = auth_manager.authenticate_user(request.username, request.password)
|
| 318 |
+
if result["success"]:
|
| 319 |
+
return {
|
| 320 |
+
"success": True,
|
| 321 |
+
"token": result["token"],
|
| 322 |
+
"user_id": result["user_id"],
|
| 323 |
+
"username": result["username"]
|
| 324 |
+
}
|
| 325 |
+
else:
|
| 326 |
+
raise HTTPException(status_code=401, detail=result["error"])
|
| 327 |
+
|
| 328 |
+
@app.get("/login", response_class=HTMLResponse)
|
| 329 |
+
async def login_page(request: Request):
|
| 330 |
+
# Check if ResearchMate is initialized
|
| 331 |
+
global research_mate_initialized
|
| 332 |
+
if not research_mate_initialized:
|
| 333 |
+
return RedirectResponse(url="/loading", status_code=302)
|
| 334 |
+
|
| 335 |
+
return templates.TemplateResponse("login.html", {"request": request})
|
| 336 |
+
|
| 337 |
+
@app.post("/api/auth/logout")
|
| 338 |
+
async def logout(request: Request):
|
| 339 |
+
# Get current user to invalidate their session
|
| 340 |
+
user = await get_current_user_web(request)
|
| 341 |
+
if user:
|
| 342 |
+
auth_manager.logout_user(user['user_id'])
|
| 343 |
+
|
| 344 |
+
response = JSONResponse({"success": True, "message": "Logged out successfully"})
|
| 345 |
+
response.delete_cookie("authToken", path="/")
|
| 346 |
+
return response
|
| 347 |
+
|
| 348 |
+
# Web interface routes (protected)
|
| 349 |
+
@app.get("/", response_class=HTMLResponse)
|
| 350 |
+
async def home(request: Request):
|
| 351 |
+
# Check if ResearchMate is initialized first
|
| 352 |
+
global research_mate_initialized
|
| 353 |
+
if not research_mate_initialized:
|
| 354 |
+
return RedirectResponse(url="/loading", status_code=302)
|
| 355 |
+
|
| 356 |
+
# Check if user is authenticated
|
| 357 |
+
user = await get_current_user_web(request)
|
| 358 |
+
if not user:
|
| 359 |
+
return RedirectResponse(url="/login", status_code=302)
|
| 360 |
+
return templates.TemplateResponse("index.html", {"request": request, "user": user})
|
| 361 |
+
|
| 362 |
+
@app.get("/search", response_class=HTMLResponse)
|
| 363 |
+
async def search_page(request: Request):
|
| 364 |
+
# Check if ResearchMate is initialized first
|
| 365 |
+
global research_mate_initialized
|
| 366 |
+
if not research_mate_initialized:
|
| 367 |
+
return RedirectResponse(url="/loading", status_code=302)
|
| 368 |
+
|
| 369 |
+
user = await get_current_user_web(request)
|
| 370 |
+
if not user:
|
| 371 |
+
return RedirectResponse(url="/login", status_code=302)
|
| 372 |
+
return templates.TemplateResponse("search.html", {"request": request, "user": user})
|
| 373 |
+
|
| 374 |
+
@app.get("/projects", response_class=HTMLResponse)
|
| 375 |
+
async def projects_page(request: Request):
|
| 376 |
+
user = await get_current_user_web(request)
|
| 377 |
+
if not user:
|
| 378 |
+
return RedirectResponse(url="/login", status_code=302)
|
| 379 |
+
return templates.TemplateResponse("projects.html", {"request": request, "user": user})
|
| 380 |
+
|
| 381 |
+
@app.get("/trends", response_class=HTMLResponse)
|
| 382 |
+
async def trends_page(request: Request):
|
| 383 |
+
user = await get_current_user_web(request)
|
| 384 |
+
if not user:
|
| 385 |
+
return RedirectResponse(url="/login", status_code=302)
|
| 386 |
+
return templates.TemplateResponse("trends.html", {"request": request, "user": user})
|
| 387 |
+
|
| 388 |
+
@app.get("/upload", response_class=HTMLResponse)
|
| 389 |
+
async def upload_page(request: Request):
|
| 390 |
+
user = await get_current_user_web(request)
|
| 391 |
+
if not user:
|
| 392 |
+
return RedirectResponse(url="/login", status_code=302)
|
| 393 |
+
return templates.TemplateResponse("upload.html", {"request": request, "user": user})
|
| 394 |
+
|
| 395 |
+
@app.get("/citation", response_class=HTMLResponse)
|
| 396 |
+
async def citation_page(request: Request):
|
| 397 |
+
try:
|
| 398 |
+
if citation_analyzer is None:
|
| 399 |
+
# If citation analyzer isn't initialized yet, show empty state
|
| 400 |
+
summary = {"total_papers": 0, "total_citations": 0, "networks": []}
|
| 401 |
+
else:
|
| 402 |
+
summary = citation_analyzer.get_network_summary()
|
| 403 |
+
return templates.TemplateResponse("citation.html", {"request": request, "summary": summary})
|
| 404 |
+
except Exception as e:
|
| 405 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 406 |
+
|
| 407 |
+
@app.get("/test-search", response_class=HTMLResponse)
|
| 408 |
+
async def test_search_page(request: Request):
|
| 409 |
+
"""Simple test page for debugging search"""
|
| 410 |
+
with open("test_search.html", "r") as f:
|
| 411 |
+
content = f.read()
|
| 412 |
+
return HTMLResponse(content=content)
|
| 413 |
+
|
| 414 |
+
# Health check endpoint for Azure
|
| 415 |
+
@app.get("/health")
|
| 416 |
+
async def health_check():
|
| 417 |
+
"""Health check endpoint for Azure and other platforms"""
|
| 418 |
+
return {"status": "ok", "timestamp": datetime.now().isoformat()}
|
| 419 |
+
|
| 420 |
+
# API endpoints
|
| 421 |
+
@app.post("/api/search")
|
| 422 |
+
async def search_papers(query: SearchQuery, current_user: dict = Depends(get_current_user_dependency)):
|
| 423 |
+
try:
|
| 424 |
+
if research_mate is None:
|
| 425 |
+
raise HTTPException(status_code=503, detail="ResearchMate not initialized")
|
| 426 |
+
rm = research_mate
|
| 427 |
+
result = rm.search(query.query, query.max_results)
|
| 428 |
+
if not result.get("success"):
|
| 429 |
+
raise HTTPException(status_code=400, detail=result.get("error", "Search failed"))
|
| 430 |
+
papers = result.get("papers", [])
|
| 431 |
+
if papers and citation_analyzer is not None: # Only add papers if citation analyzer is ready
|
| 432 |
+
citation_analyzer.add_papers(papers)
|
| 433 |
+
return result
|
| 434 |
+
except Exception as e:
|
| 435 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 436 |
+
|
| 437 |
+
@app.post("/api/ask")
|
| 438 |
+
async def ask_question(question: QuestionQuery, current_user: dict = Depends(get_current_user_dependency)):
|
| 439 |
+
try:
|
| 440 |
+
if research_mate is None:
|
| 441 |
+
raise HTTPException(status_code=503, detail="ResearchMate not initialized")
|
| 442 |
+
rm = research_mate
|
| 443 |
+
result = rm.ask(question.question)
|
| 444 |
+
if not result.get("success"):
|
| 445 |
+
raise HTTPException(status_code=400, detail=result.get("error", "Question failed"))
|
| 446 |
+
return result
|
| 447 |
+
except Exception as e:
|
| 448 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 449 |
+
|
| 450 |
+
@app.post("/api/upload")
|
| 451 |
+
async def upload_pdf(file: UploadFile = File(...), current_user: dict = Depends(get_current_user_dependency)):
|
| 452 |
+
if research_mate is None:
|
| 453 |
+
raise HTTPException(status_code=503, detail="ResearchMate not initialized")
|
| 454 |
+
|
| 455 |
+
if not file.filename.endswith('.pdf'):
|
| 456 |
+
raise HTTPException(status_code=400, detail="Only PDF files are supported")
|
| 457 |
+
|
| 458 |
+
try:
|
| 459 |
+
# Save uploaded file to /tmp/researchmate/uploads
|
| 460 |
+
upload_dir = Path(settings.get_upload_dir())
|
| 461 |
+
upload_dir.mkdir(exist_ok=True)
|
| 462 |
+
file_path = upload_dir / file.filename
|
| 463 |
+
with open(file_path, "wb") as buffer:
|
| 464 |
+
content = await file.read()
|
| 465 |
+
buffer.write(content)
|
| 466 |
+
# Process PDF
|
| 467 |
+
result = research_mate.upload_pdf(str(file_path))
|
| 468 |
+
# Clean up file
|
| 469 |
+
file_path.unlink()
|
| 470 |
+
if not result.get("success"):
|
| 471 |
+
raise HTTPException(status_code=400, detail=result.get("error", "PDF analysis failed"))
|
| 472 |
+
return result
|
| 473 |
+
except Exception as e:
|
| 474 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 475 |
+
|
| 476 |
+
@app.post("/api/projects")
|
| 477 |
+
async def create_project(project: ProjectCreate, current_user: dict = Depends(get_current_user_dependency)):
|
| 478 |
+
if research_mate is None:
|
| 479 |
+
raise HTTPException(status_code=503, detail="ResearchMate not initialized")
|
| 480 |
+
|
| 481 |
+
try:
|
| 482 |
+
user_id = current_user.get("user_id")
|
| 483 |
+
result = research_mate.create_project(project.name, project.research_question, project.keywords, user_id)
|
| 484 |
+
if not result.get("success"):
|
| 485 |
+
raise HTTPException(status_code=400, detail=result.get("error", "Project creation failed"))
|
| 486 |
+
return result
|
| 487 |
+
except Exception as e:
|
| 488 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 489 |
+
|
| 490 |
+
@app.get("/api/projects")
|
| 491 |
+
async def list_projects(current_user: dict = Depends(get_current_user_dependency)):
|
| 492 |
+
if research_mate is None:
|
| 493 |
+
raise HTTPException(status_code=503, detail="ResearchMate not initialized")
|
| 494 |
+
|
| 495 |
+
try:
|
| 496 |
+
user_id = current_user.get("user_id")
|
| 497 |
+
result = research_mate.list_projects(user_id)
|
| 498 |
+
if not result.get("success"):
|
| 499 |
+
raise HTTPException(status_code=400, detail=result.get("error", "Failed to list projects"))
|
| 500 |
+
return result
|
| 501 |
+
except Exception as e:
|
| 502 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 503 |
+
|
| 504 |
+
@app.get("/api/projects/{project_id}")
|
| 505 |
+
async def get_project(project_id: str, current_user: dict = Depends(get_current_user_dependency)):
|
| 506 |
+
if research_mate is None:
|
| 507 |
+
raise HTTPException(status_code=503, detail="ResearchMate not initialized")
|
| 508 |
+
|
| 509 |
+
try:
|
| 510 |
+
user_id = current_user.get("user_id")
|
| 511 |
+
result = research_mate.get_project(project_id, user_id)
|
| 512 |
+
if not result.get("success"):
|
| 513 |
+
raise HTTPException(status_code=404, detail=result.get("error", "Project not found"))
|
| 514 |
+
return result
|
| 515 |
+
except Exception as e:
|
| 516 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 517 |
+
|
| 518 |
+
@app.post("/api/projects/{project_id}/search")
|
| 519 |
+
async def search_project_literature(project_id: str, max_papers: int = 10, current_user: dict = Depends(get_current_user_dependency)):
|
| 520 |
+
if research_mate is None:
|
| 521 |
+
raise HTTPException(status_code=503, detail="ResearchMate not initialized")
|
| 522 |
+
|
| 523 |
+
try:
|
| 524 |
+
user_id = current_user.get("user_id")
|
| 525 |
+
result = research_mate.search_project_literature(project_id, max_papers, user_id)
|
| 526 |
+
if not result.get("success"):
|
| 527 |
+
raise HTTPException(status_code=400, detail=result.get("error", "Literature search failed"))
|
| 528 |
+
return result
|
| 529 |
+
except Exception as e:
|
| 530 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 531 |
+
|
| 532 |
+
@app.post("/api/projects/{project_id}/analyze")
|
| 533 |
+
async def analyze_project(project_id: str, current_user: dict = Depends(get_current_user_dependency)):
|
| 534 |
+
if research_mate is None:
|
| 535 |
+
raise HTTPException(status_code=503, detail="ResearchMate not initialized")
|
| 536 |
+
|
| 537 |
+
try:
|
| 538 |
+
user_id = current_user.get("user_id")
|
| 539 |
+
result = research_mate.analyze_project(project_id, user_id)
|
| 540 |
+
if not result.get("success"):
|
| 541 |
+
raise HTTPException(status_code=400, detail=result.get("error", "Project analysis failed"))
|
| 542 |
+
return result
|
| 543 |
+
except Exception as e:
|
| 544 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 545 |
+
|
| 546 |
+
@app.post("/api/projects/{project_id}/review")
|
| 547 |
+
async def generate_review(project_id: str, current_user: dict = Depends(get_current_user_dependency)):
|
| 548 |
+
if research_mate is None:
|
| 549 |
+
raise HTTPException(status_code=503, detail="ResearchMate not initialized")
|
| 550 |
+
|
| 551 |
+
try:
|
| 552 |
+
user_id = current_user.get("user_id")
|
| 553 |
+
result = research_mate.generate_review(project_id, user_id)
|
| 554 |
+
if not result.get("success"):
|
| 555 |
+
raise HTTPException(status_code=400, detail=result.get("error", "Review generation failed"))
|
| 556 |
+
return result
|
| 557 |
+
except Exception as e:
|
| 558 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 559 |
+
|
| 560 |
+
@app.post("/api/projects/{project_id}/ask")
|
| 561 |
+
async def ask_project_question(project_id: str, question: QuestionQuery):
|
| 562 |
+
if research_mate is None:
|
| 563 |
+
raise HTTPException(status_code=503, detail="ResearchMate not initialized")
|
| 564 |
+
|
| 565 |
+
try:
|
| 566 |
+
result = research_mate.ask_project_question(project_id, question.question)
|
| 567 |
+
if not result.get("success"):
|
| 568 |
+
raise HTTPException(status_code=400, detail=result.get("error", "Project question failed"))
|
| 569 |
+
return result
|
| 570 |
+
except Exception as e:
|
| 571 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 572 |
+
|
| 573 |
+
|
| 574 |
+
|
| 575 |
+
@app.post("/api/trends")
|
| 576 |
+
async def get_trends(trend: TrendQuery):
|
| 577 |
+
if research_mate is None:
|
| 578 |
+
raise HTTPException(status_code=503, detail="ResearchMate not initialized")
|
| 579 |
+
|
| 580 |
+
try:
|
| 581 |
+
result = research_mate.analyze_trends(trend.topic)
|
| 582 |
+
if result.get("error"):
|
| 583 |
+
raise HTTPException(status_code=400, detail=result.get("error", "Trend analysis failed"))
|
| 584 |
+
return result
|
| 585 |
+
except Exception as e:
|
| 586 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 587 |
+
|
| 588 |
+
@app.post("/api/trends/temporal")
|
| 589 |
+
async def get_temporal_trends(trend: TrendQuery):
|
| 590 |
+
"""Get temporal trend analysis"""
|
| 591 |
+
if research_mate is None:
|
| 592 |
+
raise HTTPException(status_code=503, detail="ResearchMate not initialized")
|
| 593 |
+
|
| 594 |
+
try:
|
| 595 |
+
# Get papers for analysis
|
| 596 |
+
papers = research_mate.search_papers(trend.topic, 50)
|
| 597 |
+
if not papers:
|
| 598 |
+
raise HTTPException(status_code=404, detail="No papers found for temporal analysis")
|
| 599 |
+
|
| 600 |
+
# Use advanced trend monitor
|
| 601 |
+
result = research_mate.trend_monitor.analyze_temporal_trends(papers)
|
| 602 |
+
if result.get("error"):
|
| 603 |
+
raise HTTPException(status_code=400, detail=result.get("error"))
|
| 604 |
+
|
| 605 |
+
return {
|
| 606 |
+
"topic": trend.topic,
|
| 607 |
+
"temporal_analysis": result,
|
| 608 |
+
"papers_analyzed": len(papers)
|
| 609 |
+
}
|
| 610 |
+
except Exception as e:
|
| 611 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 612 |
+
|
| 613 |
+
@app.post("/api/trends/gaps")
|
| 614 |
+
async def detect_research_gaps(trend: TrendQuery):
|
| 615 |
+
"""Detect research gaps for a topic"""
|
| 616 |
+
if research_mate is None:
|
| 617 |
+
raise HTTPException(status_code=503, detail="ResearchMate not initialized")
|
| 618 |
+
|
| 619 |
+
try:
|
| 620 |
+
# Get papers for gap analysis
|
| 621 |
+
papers = research_mate.search_papers(trend.topic, 50)
|
| 622 |
+
if not papers:
|
| 623 |
+
raise HTTPException(status_code=404, detail="No papers found for gap analysis")
|
| 624 |
+
|
| 625 |
+
# Use advanced trend monitor
|
| 626 |
+
result = research_mate.trend_monitor.detect_research_gaps(papers)
|
| 627 |
+
if result.get("error"):
|
| 628 |
+
raise HTTPException(status_code=400, detail=result.get("error"))
|
| 629 |
+
|
| 630 |
+
return {
|
| 631 |
+
"topic": trend.topic,
|
| 632 |
+
"gap_analysis": result,
|
| 633 |
+
"papers_analyzed": len(papers)
|
| 634 |
+
}
|
| 635 |
+
except Exception as e:
|
| 636 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 637 |
+
|
| 638 |
+
@app.get("/api/status")
|
| 639 |
+
async def get_status(current_user: dict = Depends(get_current_user_dependency)):
|
| 640 |
+
if research_mate is None:
|
| 641 |
+
raise HTTPException(status_code=503, detail="ResearchMate not initialized")
|
| 642 |
+
|
| 643 |
+
try:
|
| 644 |
+
result = research_mate.get_status()
|
| 645 |
+
# Ensure proper structure for frontend
|
| 646 |
+
if result.get('success'):
|
| 647 |
+
return {
|
| 648 |
+
'success': True,
|
| 649 |
+
'statistics': result.get('statistics', {
|
| 650 |
+
'rag_documents': 0,
|
| 651 |
+
'system_version': '1.0.0',
|
| 652 |
+
'status_check_time': datetime.now().isoformat()
|
| 653 |
+
}),
|
| 654 |
+
'components': result.get('components', {})
|
| 655 |
+
}
|
| 656 |
+
else:
|
| 657 |
+
return result
|
| 658 |
+
except Exception as e:
|
| 659 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 660 |
+
|
| 661 |
+
# Initialization status endpoint
|
| 662 |
+
@app.get("/api/init-status")
|
| 663 |
+
async def get_init_status():
|
| 664 |
+
"""Check if ResearchMate is initialized"""
|
| 665 |
+
global research_mate_initialized, initialization_in_progress
|
| 666 |
+
|
| 667 |
+
if research_mate_initialized:
|
| 668 |
+
status = "ready"
|
| 669 |
+
elif initialization_in_progress:
|
| 670 |
+
status = "initializing"
|
| 671 |
+
else:
|
| 672 |
+
status = "not_started"
|
| 673 |
+
|
| 674 |
+
return {
|
| 675 |
+
"initialized": research_mate_initialized,
|
| 676 |
+
"in_progress": initialization_in_progress,
|
| 677 |
+
"timestamp": datetime.now().isoformat(),
|
| 678 |
+
"status": status
|
| 679 |
+
}
|
| 680 |
+
|
| 681 |
+
# Fast search endpoint that initializes on first call
|
| 682 |
+
@app.post("/api/search-fast")
|
| 683 |
+
async def search_papers_fast(query: SearchQuery, current_user: dict = Depends(get_current_user_dependency)):
|
| 684 |
+
"""Fast search that shows initialization progress"""
|
| 685 |
+
try:
|
| 686 |
+
global research_mate
|
| 687 |
+
if research_mate is None:
|
| 688 |
+
# Return immediate response indicating initialization
|
| 689 |
+
return {
|
| 690 |
+
"initializing": True,
|
| 691 |
+
"message": "ResearchMate is initializing (this may take 30-60 seconds)...",
|
| 692 |
+
"query": query.query,
|
| 693 |
+
"estimated_time": "30-60 seconds"
|
| 694 |
+
}
|
| 695 |
+
|
| 696 |
+
# Use existing search
|
| 697 |
+
result = research_mate.search(query.query, query.max_results)
|
| 698 |
+
if not result.get("success"):
|
| 699 |
+
raise HTTPException(status_code=400, detail=result.get("error", "Search failed"))
|
| 700 |
+
|
| 701 |
+
papers = result.get("papers", [])
|
| 702 |
+
if papers and citation_analyzer is not None:
|
| 703 |
+
citation_analyzer.add_papers(papers)
|
| 704 |
+
|
| 705 |
+
return result
|
| 706 |
+
except Exception as e:
|
| 707 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 708 |
+
|
| 709 |
+
@app.get("/api/user/status")
|
| 710 |
+
async def get_user_status(current_user: dict = Depends(get_current_user_dependency)):
|
| 711 |
+
"""Get current user's status and statistics"""
|
| 712 |
+
if research_mate is None:
|
| 713 |
+
raise HTTPException(status_code=503, detail="ResearchMate not initialized")
|
| 714 |
+
|
| 715 |
+
try:
|
| 716 |
+
user_id = current_user.get("user_id")
|
| 717 |
+
|
| 718 |
+
# Get user's projects
|
| 719 |
+
projects_result = research_mate.list_projects(user_id)
|
| 720 |
+
if not projects_result.get("success"):
|
| 721 |
+
raise HTTPException(status_code=400, detail="Failed to get user projects")
|
| 722 |
+
|
| 723 |
+
user_projects = projects_result.get("projects", [])
|
| 724 |
+
total_papers = sum(len(p.get('papers', [])) for p in user_projects)
|
| 725 |
+
|
| 726 |
+
return {
|
| 727 |
+
"success": True,
|
| 728 |
+
"user_id": user_id,
|
| 729 |
+
"username": current_user.get("username"),
|
| 730 |
+
"statistics": {
|
| 731 |
+
"total_projects": len(user_projects),
|
| 732 |
+
"total_papers": total_papers,
|
| 733 |
+
"active_projects": len([p for p in user_projects if p.get('status') == 'active'])
|
| 734 |
+
},
|
| 735 |
+
"last_updated": datetime.now().isoformat()
|
| 736 |
+
}
|
| 737 |
+
except Exception as e:
|
| 738 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 739 |
+
|
| 740 |
+
# Trigger initialization endpoint (for testing)
|
| 741 |
+
@app.post("/api/trigger-init")
|
| 742 |
+
async def trigger_initialization():
|
| 743 |
+
"""Manually trigger ResearchMate initialization"""
|
| 744 |
+
if not initialization_in_progress and not research_mate_initialized:
|
| 745 |
+
asyncio.create_task(initialize_research_mate())
|
| 746 |
+
return {"message": "Initialization triggered"}
|
| 747 |
+
elif initialization_in_progress:
|
| 748 |
+
return {"message": "Initialization already in progress"}
|
| 749 |
+
else:
|
| 750 |
+
return {"message": "Already initialized"}
|
| 751 |
+
|
| 752 |
+
# Legacy health check endpoint
|
| 753 |
+
@app.get("/api/health")
|
| 754 |
+
async def api_health_check():
|
| 755 |
+
"""Legacy health check endpoint"""
|
| 756 |
+
return {"status": "ok", "timestamp": datetime.now().isoformat()}
|
| 757 |
+
|
| 758 |
+
# Update the existing FastAPI app to use lifespan
|
| 759 |
+
app.router.lifespan_context = lifespan
|
| 760 |
+
|
| 761 |
+
# Startup event to ensure initialization begins immediately after server starts
|
| 762 |
+
@app.on_event("startup")
|
| 763 |
+
async def startup_event():
|
| 764 |
+
"""Ensure initialization starts on startup"""
|
| 765 |
+
print("🌟 Server started, ensuring ResearchMate initialization begins...")
|
| 766 |
+
# Give the server a moment to fully start, then trigger initialization
|
| 767 |
+
await asyncio.sleep(1)
|
| 768 |
+
if not initialization_in_progress and not research_mate_initialized:
|
| 769 |
+
asyncio.create_task(initialize_research_mate())
|
| 770 |
+
|
| 771 |
+
|
| 772 |
+
# Local development: run with `python app.py`
|
| 773 |
+
if __name__ == "__main__":
|
| 774 |
+
import uvicorn
|
| 775 |
+
port = settings.server.port
|
| 776 |
+
host = settings.server.host
|
| 777 |
+
print(f"\nStarting ResearchMate locally at http://{host}:{port}\n")
|
| 778 |
+
uvicorn.run(
|
| 779 |
+
"app:app",
|
| 780 |
+
host=host,
|
| 781 |
+
port=port,
|
| 782 |
+
log_level="info",
|
| 783 |
+
reload=True
|
| 784 |
+
)
|
data/active_sessions.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
{
|
| 2 |
"admin_user": {
|
| 3 |
-
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
|
| 4 |
-
"created_at": "2025-07-
|
| 5 |
-
"last_activity": "2025-07-
|
| 6 |
}
|
| 7 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"admin_user": {
|
| 3 |
+
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiYWRtaW5fdXNlciIsInVzZXJuYW1lIjoiYWRtaW4iLCJleHAiOjE3NTI2MTI4NjR9.-4rWv6qNOaSp9kup3AjqbwhC_h5P6anYhxP6OfYoBWU",
|
| 4 |
+
"created_at": "2025-07-15T18:24:24.155119",
|
| 5 |
+
"last_activity": "2025-07-15T18:26:51.425464"
|
| 6 |
}
|
| 7 |
}
|
main.py
DELETED
|
@@ -1,234 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
ResearchMate - Main Application Entry Point
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import os
|
| 7 |
-
import sys
|
| 8 |
-
import logging
|
| 9 |
-
from pathlib import Path
|
| 10 |
-
|
| 11 |
-
# Set up environment variables before importing anything else
|
| 12 |
-
def setup_environment():
|
| 13 |
-
"""Configure environment variables for writable paths"""
|
| 14 |
-
# Force all paths to writable locations
|
| 15 |
-
env_vars = {
|
| 16 |
-
'DATA_DIR': '/tmp/researchmate/data',
|
| 17 |
-
'LOGS_DIR': '/tmp/researchmate/logs',
|
| 18 |
-
'CHROMA_DIR': '/tmp/researchmate/chroma_persist',
|
| 19 |
-
'UPLOADS_DIR': '/tmp/researchmate/uploads',
|
| 20 |
-
'CHROMA_DB_DIR': '/tmp/researchmate/chroma_db',
|
| 21 |
-
'CONFIG_DIR': '/tmp/researchmate/config',
|
| 22 |
-
'TEMP_DIR': '/tmp/researchmate/tmp',
|
| 23 |
-
'CHROMA_PERSIST_DIR': '/tmp/researchmate/chroma_persist', # Additional key
|
| 24 |
-
|
| 25 |
-
# Cache directories
|
| 26 |
-
'MPLCONFIGDIR': '/tmp/matplotlib',
|
| 27 |
-
'TRANSFORMERS_CACHE': '/tmp/transformers',
|
| 28 |
-
'HF_HOME': '/tmp/huggingface',
|
| 29 |
-
'SENTENCE_TRANSFORMERS_HOME': '/tmp/sentence_transformers',
|
| 30 |
-
'HF_DATASETS_CACHE': '/tmp/datasets',
|
| 31 |
-
'HUGGINGFACE_HUB_CACHE': '/tmp/huggingface_hub',
|
| 32 |
-
'XDG_CACHE_HOME': '/tmp/cache',
|
| 33 |
-
|
| 34 |
-
# Additional variables to prevent /data access
|
| 35 |
-
'PYTORCH_KERNEL_CACHE_PATH': '/tmp/cache',
|
| 36 |
-
'TORCH_HOME': '/tmp/cache',
|
| 37 |
-
'NLTK_DATA': '/tmp/cache/nltk_data',
|
| 38 |
-
'TOKENIZERS_PARALLELISM': 'false',
|
| 39 |
-
|
| 40 |
-
# Override any hardcoded paths
|
| 41 |
-
'HOME': '/tmp/cache',
|
| 42 |
-
'TMPDIR': '/tmp/researchmate/tmp',
|
| 43 |
-
|
| 44 |
-
# HF Spaces specific - prevent access to /data
|
| 45 |
-
'HF_DATASETS_OFFLINE': '1',
|
| 46 |
-
'HF_HUB_OFFLINE': '0',
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
for key, value in env_vars.items():
|
| 50 |
-
os.environ[key] = value # Force set all environment variables
|
| 51 |
-
|
| 52 |
-
# Also set any Python path variables
|
| 53 |
-
sys.path.insert(0, '/tmp/cache')
|
| 54 |
-
|
| 55 |
-
# Create directories if they don't exist
|
| 56 |
-
directories = [
|
| 57 |
-
'/tmp/researchmate/data',
|
| 58 |
-
'/tmp/researchmate/logs',
|
| 59 |
-
'/tmp/researchmate/chroma_persist',
|
| 60 |
-
'/tmp/researchmate/uploads',
|
| 61 |
-
'/tmp/researchmate/chroma_db',
|
| 62 |
-
'/tmp/researchmate/config',
|
| 63 |
-
'/tmp/researchmate/tmp',
|
| 64 |
-
'/tmp/matplotlib',
|
| 65 |
-
'/tmp/transformers',
|
| 66 |
-
'/tmp/huggingface',
|
| 67 |
-
'/tmp/sentence_transformers',
|
| 68 |
-
'/tmp/datasets',
|
| 69 |
-
'/tmp/huggingface_hub',
|
| 70 |
-
'/tmp/cache',
|
| 71 |
-
'/tmp/cache/nltk_data'
|
| 72 |
-
]
|
| 73 |
-
|
| 74 |
-
for directory in directories:
|
| 75 |
-
try:
|
| 76 |
-
path = Path(directory)
|
| 77 |
-
path.mkdir(parents=True, exist_ok=True)
|
| 78 |
-
# Ensure write permissions
|
| 79 |
-
path.chmod(0o777)
|
| 80 |
-
print(f"✓ Created/verified directory: {directory}")
|
| 81 |
-
except Exception as e:
|
| 82 |
-
print(f"⚠ Warning: Could not create directory {directory}: {e}")
|
| 83 |
-
|
| 84 |
-
# Set up environment FIRST, before any imports
|
| 85 |
-
setup_environment()
|
| 86 |
-
|
| 87 |
-
# Now import other modules
|
| 88 |
-
import uvicorn
|
| 89 |
-
from fastapi import FastAPI
|
| 90 |
-
from fastapi.staticfiles import StaticFiles
|
| 91 |
-
from fastapi.middleware.cors import CORSMiddleware
|
| 92 |
-
from fastapi.middleware.gzip import GZipMiddleware
|
| 93 |
-
from fastapi.responses import JSONResponse
|
| 94 |
-
|
| 95 |
-
# Configure logging early
|
| 96 |
-
log_file = os.path.join(os.environ.get('LOGS_DIR', '/tmp/researchmate/logs'), 'app.log')
|
| 97 |
-
logging.basicConfig(
|
| 98 |
-
level=logging.INFO,
|
| 99 |
-
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
| 100 |
-
handlers=[
|
| 101 |
-
logging.StreamHandler(sys.stdout),
|
| 102 |
-
logging.FileHandler(log_file, mode='a')
|
| 103 |
-
]
|
| 104 |
-
)
|
| 105 |
-
|
| 106 |
-
logger = logging.getLogger(__name__)
|
| 107 |
-
|
| 108 |
-
def main():
|
| 109 |
-
"""Main application entry point"""
|
| 110 |
-
try:
|
| 111 |
-
print("===== ResearchMate Application Startup =====")
|
| 112 |
-
print("Setting up environment...")
|
| 113 |
-
|
| 114 |
-
# Double-check environment is properly set
|
| 115 |
-
print(f"CHROMA_DIR: {os.environ.get('CHROMA_DIR')}")
|
| 116 |
-
print(f"UPLOADS_DIR: {os.environ.get('UPLOADS_DIR')}")
|
| 117 |
-
print(f"LOGS_DIR: {os.environ.get('LOGS_DIR')}")
|
| 118 |
-
print(f"HF_HOME: {os.environ.get('HF_HOME')}")
|
| 119 |
-
|
| 120 |
-
# Import settings after environment setup
|
| 121 |
-
try:
|
| 122 |
-
from src.settings import get_settings
|
| 123 |
-
settings = get_settings()
|
| 124 |
-
print(f"✓ Settings loaded successfully")
|
| 125 |
-
print(f"Database directory: {settings.database.chroma_persist_dir}")
|
| 126 |
-
except Exception as e:
|
| 127 |
-
print(f"⚠ Settings loading failed: {e}")
|
| 128 |
-
# Continue with basic settings
|
| 129 |
-
settings = None
|
| 130 |
-
|
| 131 |
-
print("Starting ResearchMate background initialization...")
|
| 132 |
-
|
| 133 |
-
# Initialize components with error handling
|
| 134 |
-
research_mate = None
|
| 135 |
-
try:
|
| 136 |
-
from src.components.research_assistant import ResearchMate
|
| 137 |
-
research_mate = ResearchMate()
|
| 138 |
-
print("✓ ResearchMate initialized successfully")
|
| 139 |
-
except Exception as e:
|
| 140 |
-
print(f"✗ Failed to initialize ResearchMate: {e}")
|
| 141 |
-
import traceback
|
| 142 |
-
traceback.print_exc()
|
| 143 |
-
print("⚠ Server will start but ResearchMate features may not work")
|
| 144 |
-
|
| 145 |
-
# Create FastAPI app
|
| 146 |
-
app = FastAPI(
|
| 147 |
-
title="ResearchMate",
|
| 148 |
-
description="AI-powered research assistant",
|
| 149 |
-
version="1.0.0"
|
| 150 |
-
)
|
| 151 |
-
|
| 152 |
-
# Add middleware
|
| 153 |
-
if settings:
|
| 154 |
-
app.add_middleware(
|
| 155 |
-
CORSMiddleware,
|
| 156 |
-
allow_origins=settings.security.cors_origins,
|
| 157 |
-
allow_credentials=True,
|
| 158 |
-
allow_methods=settings.security.cors_methods,
|
| 159 |
-
allow_headers=settings.security.cors_headers,
|
| 160 |
-
)
|
| 161 |
-
else:
|
| 162 |
-
# Basic CORS for HF Spaces
|
| 163 |
-
app.add_middleware(
|
| 164 |
-
CORSMiddleware,
|
| 165 |
-
allow_origins=["*"],
|
| 166 |
-
allow_credentials=True,
|
| 167 |
-
allow_methods=["*"],
|
| 168 |
-
allow_headers=["*"],
|
| 169 |
-
)
|
| 170 |
-
|
| 171 |
-
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
| 172 |
-
|
| 173 |
-
# Health check endpoint
|
| 174 |
-
@app.get("/health")
|
| 175 |
-
async def health_check():
|
| 176 |
-
return JSONResponse({
|
| 177 |
-
"status": "healthy",
|
| 178 |
-
"version": "1.0.0",
|
| 179 |
-
"chroma_dir": os.environ.get('CHROMA_DIR'),
|
| 180 |
-
"writable_test": "OK"
|
| 181 |
-
})
|
| 182 |
-
|
| 183 |
-
# Basic root endpoint
|
| 184 |
-
@app.get("/")
|
| 185 |
-
async def root():
|
| 186 |
-
return JSONResponse({
|
| 187 |
-
"message": "ResearchMate API",
|
| 188 |
-
"status": "running",
|
| 189 |
-
"research_mate_available": research_mate is not None
|
| 190 |
-
})
|
| 191 |
-
|
| 192 |
-
# Mount static files if available
|
| 193 |
-
try:
|
| 194 |
-
if settings:
|
| 195 |
-
static_dir = settings.get_static_dir()
|
| 196 |
-
else:
|
| 197 |
-
static_dir = "src/static"
|
| 198 |
-
|
| 199 |
-
if Path(static_dir).exists():
|
| 200 |
-
app.mount("/static", StaticFiles(directory=static_dir), name="static")
|
| 201 |
-
print(f"✓ Static files mounted from: {static_dir}")
|
| 202 |
-
except Exception as e:
|
| 203 |
-
logger.warning(f"Could not mount static files: {e}")
|
| 204 |
-
|
| 205 |
-
# No API routers to include (src.api.routes does not exist)
|
| 206 |
-
# If you add API routers in the future, include them here.
|
| 207 |
-
|
| 208 |
-
# For Hugging Face Spaces, use port 7860
|
| 209 |
-
port = int(os.environ.get("PORT", 7860))
|
| 210 |
-
host = os.environ.get("HOST", "0.0.0.0")
|
| 211 |
-
|
| 212 |
-
print(f"🚀 Starting server on {host}:{port}")
|
| 213 |
-
if settings:
|
| 214 |
-
print(f"📁 Data directory: {settings.database.chroma_persist_dir}")
|
| 215 |
-
print(f"📤 Upload directory: {settings.get_upload_dir()}")
|
| 216 |
-
print(f"🔧 Config file: {settings.config_file}")
|
| 217 |
-
|
| 218 |
-
# Start the server
|
| 219 |
-
uvicorn.run(
|
| 220 |
-
app,
|
| 221 |
-
host=host,
|
| 222 |
-
port=port,
|
| 223 |
-
log_level="info",
|
| 224 |
-
access_log=True
|
| 225 |
-
)
|
| 226 |
-
|
| 227 |
-
except Exception as e:
|
| 228 |
-
logger.error(f"Failed to start application: {e}")
|
| 229 |
-
import traceback
|
| 230 |
-
traceback.print_exc()
|
| 231 |
-
sys.exit(1)
|
| 232 |
-
|
| 233 |
-
if __name__ == "__main__":
|
| 234 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
requirements.txt
CHANGED
|
@@ -26,4 +26,4 @@ aiofiles
|
|
| 26 |
watchdog
|
| 27 |
seaborn
|
| 28 |
PyJWT
|
| 29 |
-
flask
|
|
|
|
| 26 |
watchdog
|
| 27 |
seaborn
|
| 28 |
PyJWT
|
| 29 |
+
flask
|
src/components/__pycache__/auth.cpython-311.pyc
CHANGED
|
Binary files a/src/components/__pycache__/auth.cpython-311.pyc and b/src/components/__pycache__/auth.cpython-311.pyc differ
|
|
|
src/components/__pycache__/config.cpython-311.pyc
CHANGED
|
Binary files a/src/components/__pycache__/config.cpython-311.pyc and b/src/components/__pycache__/config.cpython-311.pyc differ
|
|
|
src/components/auth.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
"""
|
| 4 |
|
| 5 |
import jwt
|
|
@@ -8,66 +8,34 @@ import json
|
|
| 8 |
import os
|
| 9 |
from datetime import datetime, timedelta
|
| 10 |
from typing import Optional, Dict, Any
|
| 11 |
-
from
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
class AuthManager:
|
| 14 |
def __init__(self, secret_key: str = None):
|
| 15 |
self.secret_key = secret_key or os.environ.get('SECRET_KEY', 'dev-secret-key-change-in-production')
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
self.
|
| 19 |
-
self.data_dir = self.base_dir / 'data'
|
| 20 |
-
|
| 21 |
-
# Fallback to current directory if base_dir doesn't work
|
| 22 |
-
if not self.data_dir.exists():
|
| 23 |
-
self.data_dir = Path.cwd() / 'data'
|
| 24 |
-
|
| 25 |
-
self.users_file = self.data_dir / 'users.json'
|
| 26 |
-
self.session_file = self.data_dir / 'active_sessions.json'
|
| 27 |
-
|
| 28 |
-
# In-memory fallback for read-only environments
|
| 29 |
-
self.users_memory = {}
|
| 30 |
-
self.sessions_memory = {}
|
| 31 |
-
self.use_memory = False
|
| 32 |
-
|
| 33 |
self.ensure_users_file()
|
| 34 |
-
self.ensure_admin_user() # Ensure admin exists on startup
|
| 35 |
-
|
| 36 |
-
print(f"✅ AuthManager initialized")
|
| 37 |
-
print(f"📁 Data directory: {self.data_dir}")
|
| 38 |
-
print(f"📄 Users file: {self.users_file}")
|
| 39 |
-
print(f"💾 Using memory storage: {self.use_memory}")
|
| 40 |
|
| 41 |
def ensure_users_file(self):
|
| 42 |
-
"""Ensure users file exists
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
with open(self.users_file, 'w') as f:
|
| 48 |
-
json.dump({}, f)
|
| 49 |
-
print(f"✅ Created users file: {self.users_file}")
|
| 50 |
-
|
| 51 |
-
# Test write permissions
|
| 52 |
-
test_data = self.load_users()
|
| 53 |
-
self.save_users(test_data)
|
| 54 |
-
print(f"✅ Write permissions confirmed")
|
| 55 |
-
|
| 56 |
-
except Exception as e:
|
| 57 |
-
print(f"⚠️ File system error: {e}")
|
| 58 |
-
print(f"🔄 Switching to in-memory storage")
|
| 59 |
-
self.use_memory = True
|
| 60 |
-
self.users_memory = {}
|
| 61 |
-
self.sessions_memory = {}
|
| 62 |
-
|
| 63 |
-
def ensure_admin_user(self):
|
| 64 |
-
"""Ensure admin user exists on startup"""
|
| 65 |
-
try:
|
| 66 |
-
result = self.create_default_admin()
|
| 67 |
-
if result.get('success'):
|
| 68 |
-
print(f"✅ Admin user ready: {result.get('message', 'Available')}")
|
| 69 |
-
except Exception as e:
|
| 70 |
-
print(f"⚠️ Admin user creation failed: {e}")
|
| 71 |
|
| 72 |
def hash_password(self, password: str) -> str:
|
| 73 |
"""Hash password with bcrypt"""
|
|
@@ -75,38 +43,20 @@ class AuthManager:
|
|
| 75 |
|
| 76 |
def verify_password(self, password: str, hashed: str) -> bool:
|
| 77 |
"""Verify password against hash"""
|
| 78 |
-
|
| 79 |
-
return bcrypt.checkpw(password.encode('utf-8'), hashed.encode('utf-8'))
|
| 80 |
-
except Exception as e:
|
| 81 |
-
print(f"❌ Password verification error: {e}")
|
| 82 |
-
return False
|
| 83 |
|
| 84 |
def load_users(self) -> Dict[str, Any]:
|
| 85 |
-
"""Load users from file
|
| 86 |
-
if self.use_memory:
|
| 87 |
-
return self.users_memory.copy()
|
| 88 |
-
|
| 89 |
try:
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
return users
|
| 94 |
-
return {}
|
| 95 |
-
except Exception as e:
|
| 96 |
-
print(f"❌ Error loading users: {e}")
|
| 97 |
return {}
|
| 98 |
|
| 99 |
def save_users(self, users: Dict[str, Any]):
|
| 100 |
-
"""Save users to file
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
return
|
| 104 |
-
|
| 105 |
-
try:
|
| 106 |
-
with open(self.users_file, 'w') as f:
|
| 107 |
-
json.dump(users, f, indent=2)
|
| 108 |
-
except Exception as e:
|
| 109 |
-
print(f"❌ Error saving users: {e}")
|
| 110 |
|
| 111 |
def create_user(self, username: str, email: str, password: str) -> Dict[str, Any]:
|
| 112 |
"""Create a new user"""
|
|
@@ -134,51 +84,38 @@ class AuthManager:
|
|
| 134 |
return {'success': True, 'user_id': user_id}
|
| 135 |
|
| 136 |
def authenticate_user(self, username: str, password: str) -> Dict[str, Any]:
|
| 137 |
-
"""Authenticate user credentials
|
| 138 |
-
print(f"🔐 Authentication attempt for: {username}")
|
| 139 |
-
|
| 140 |
users = self.load_users()
|
| 141 |
-
print(f"📊 Total users in database: {len(users)}")
|
| 142 |
|
| 143 |
if username not in users:
|
| 144 |
-
print(f"❌ Username '{username}' not found")
|
| 145 |
-
print(f"📋 Available usernames: {list(users.keys())}")
|
| 146 |
return {'success': False, 'error': 'Invalid username or password'}
|
| 147 |
|
| 148 |
user = users[username]
|
| 149 |
-
|
| 150 |
if not self.verify_password(password, user['password_hash']):
|
| 151 |
-
print(f"❌ Invalid password for '{username}'")
|
| 152 |
return {'success': False, 'error': 'Invalid username or password'}
|
| 153 |
|
| 154 |
if not user.get('is_active', True):
|
| 155 |
-
print(f"❌ User '{username}' is not active")
|
| 156 |
return {'success': False, 'error': 'Account is disabled'}
|
| 157 |
|
| 158 |
-
# Generate JWT token
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
'username': username
|
| 175 |
-
}
|
| 176 |
-
except Exception as e:
|
| 177 |
-
print(f"❌ JWT generation failed: {e}")
|
| 178 |
-
return {'success': False, 'error': 'Authentication failed'}
|
| 179 |
|
| 180 |
def verify_token(self, token: str) -> Optional[Dict[str, Any]]:
|
| 181 |
-
"""Verify JWT token"""
|
| 182 |
try:
|
| 183 |
payload = jwt.decode(token, self.secret_key, algorithms=['HS256'])
|
| 184 |
user_id = payload.get('user_id')
|
|
@@ -189,16 +126,31 @@ class AuthManager:
|
|
| 189 |
|
| 190 |
# Update session activity
|
| 191 |
self.update_session_activity(user_id)
|
|
|
|
| 192 |
return payload
|
| 193 |
except jwt.ExpiredSignatureError:
|
| 194 |
-
print("🕐 Token expired")
|
| 195 |
return None
|
| 196 |
except jwt.InvalidTokenError:
|
| 197 |
-
print("❌ Invalid token")
|
| 198 |
return None
|
| 199 |
-
|
| 200 |
-
|
|
|
|
|
|
|
| 201 |
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
|
| 203 |
def create_default_admin(self) -> Dict[str, Any]:
|
| 204 |
"""Create default admin user if it doesn't exist"""
|
|
@@ -207,56 +159,51 @@ class AuthManager:
|
|
| 207 |
admin_username = "admin"
|
| 208 |
admin_user_id = "admin_user"
|
| 209 |
|
| 210 |
-
# Check if admin already exists
|
| 211 |
if admin_username in users:
|
| 212 |
return {'success': True, 'message': 'Admin user already exists'}
|
| 213 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
# Create admin user
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
except Exception as e:
|
| 233 |
-
print(f"❌ Failed to create admin user: {e}")
|
| 234 |
-
return {'success': False, 'error': str(e)}
|
| 235 |
|
| 236 |
def load_active_sessions(self) -> Dict[str, Any]:
|
| 237 |
-
"""Load active sessions"""
|
| 238 |
-
if self.use_memory:
|
| 239 |
-
return self.sessions_memory.copy()
|
| 240 |
-
|
| 241 |
try:
|
| 242 |
-
if
|
| 243 |
with open(self.session_file, 'r') as f:
|
| 244 |
return json.load(f)
|
| 245 |
-
except
|
| 246 |
-
|
| 247 |
return {}
|
| 248 |
|
| 249 |
def save_active_sessions(self, sessions: Dict[str, Any]):
|
| 250 |
-
"""Save active sessions"""
|
| 251 |
-
if self.use_memory:
|
| 252 |
-
self.sessions_memory = sessions.copy()
|
| 253 |
-
return
|
| 254 |
-
|
| 255 |
try:
|
| 256 |
with open(self.session_file, 'w') as f:
|
| 257 |
json.dump(sessions, f, indent=2)
|
| 258 |
-
except
|
| 259 |
-
|
| 260 |
|
| 261 |
def add_active_session(self, user_id: str, token: str):
|
| 262 |
"""Add an active session"""
|
|
@@ -268,6 +215,13 @@ class AuthManager:
|
|
| 268 |
}
|
| 269 |
self.save_active_sessions(sessions)
|
| 270 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
def is_session_active(self, user_id: str, token: str) -> bool:
|
| 272 |
"""Check if a session is active"""
|
| 273 |
sessions = self.load_active_sessions()
|
|
@@ -278,31 +232,14 @@ class AuthManager:
|
|
| 278 |
if session.get('token') != token:
|
| 279 |
return False
|
| 280 |
|
| 281 |
-
# Check if session is expired
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
self.remove_active_session(user_id)
|
| 286 |
-
return False
|
| 287 |
-
except:
|
| 288 |
return False
|
| 289 |
|
| 290 |
return True
|
| 291 |
|
| 292 |
-
def remove_active_session(self, user_id: str):
|
| 293 |
-
"""Remove an active session"""
|
| 294 |
-
sessions = self.load_active_sessions()
|
| 295 |
-
if user_id in sessions:
|
| 296 |
-
del sessions[user_id]
|
| 297 |
-
self.save_active_sessions(sessions)
|
| 298 |
-
|
| 299 |
-
def update_session_activity(self, user_id: str):
|
| 300 |
-
"""Update last activity time for a session"""
|
| 301 |
-
sessions = self.load_active_sessions()
|
| 302 |
-
if user_id in sessions:
|
| 303 |
-
sessions[user_id]['last_activity'] = datetime.now().isoformat()
|
| 304 |
-
self.save_active_sessions(sessions)
|
| 305 |
-
|
| 306 |
def logout_user(self, user_id: str):
|
| 307 |
"""Logout user and invalidate session"""
|
| 308 |
self.remove_active_session(user_id)
|
|
@@ -315,11 +252,8 @@ class AuthManager:
|
|
| 315 |
|
| 316 |
expired_sessions = []
|
| 317 |
for user_id, session in sessions.items():
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
if current_time - created_at > timedelta(hours=8):
|
| 321 |
-
expired_sessions.append(user_id)
|
| 322 |
-
except:
|
| 323 |
expired_sessions.append(user_id)
|
| 324 |
|
| 325 |
for user_id in expired_sessions:
|
|
@@ -330,23 +264,34 @@ class AuthManager:
|
|
| 330 |
|
| 331 |
return len(expired_sessions)
|
| 332 |
|
| 333 |
-
def
|
| 334 |
-
"""
|
| 335 |
-
users = self.load_users()
|
| 336 |
sessions = self.load_active_sessions()
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
|
| 351 |
-
|
| 352 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
+
Authentication and authorization utilities
|
| 3 |
"""
|
| 4 |
|
| 5 |
import jwt
|
|
|
|
| 8 |
import os
|
| 9 |
from datetime import datetime, timedelta
|
| 10 |
from typing import Optional, Dict, Any
|
| 11 |
+
from functools import wraps
|
| 12 |
+
|
| 13 |
+
# Import Flask components only when available
|
| 14 |
+
try:
|
| 15 |
+
from flask import request, jsonify, session, redirect, url_for
|
| 16 |
+
FLASK_AVAILABLE = True
|
| 17 |
+
except ImportError:
|
| 18 |
+
FLASK_AVAILABLE = False
|
| 19 |
+
request = None
|
| 20 |
+
jsonify = None
|
| 21 |
+
session = None
|
| 22 |
+
redirect = None
|
| 23 |
+
url_for = None
|
| 24 |
|
| 25 |
class AuthManager:
|
| 26 |
def __init__(self, secret_key: str = None):
|
| 27 |
self.secret_key = secret_key or os.environ.get('SECRET_KEY', 'dev-secret-key-change-in-production')
|
| 28 |
+
self.users_file = 'data/users.json'
|
| 29 |
+
self.active_sessions = {} # Track active sessions for security
|
| 30 |
+
self.session_file = 'data/active_sessions.json'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
self.ensure_users_file()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
def ensure_users_file(self):
|
| 34 |
+
"""Ensure users file exists"""
|
| 35 |
+
os.makedirs('data', exist_ok=True)
|
| 36 |
+
if not os.path.exists(self.users_file):
|
| 37 |
+
with open(self.users_file, 'w') as f:
|
| 38 |
+
json.dump({}, f)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
def hash_password(self, password: str) -> str:
|
| 41 |
"""Hash password with bcrypt"""
|
|
|
|
| 43 |
|
| 44 |
def verify_password(self, password: str, hashed: str) -> bool:
|
| 45 |
"""Verify password against hash"""
|
| 46 |
+
return bcrypt.checkpw(password.encode('utf-8'), hashed.encode('utf-8'))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
def load_users(self) -> Dict[str, Any]:
|
| 49 |
+
"""Load users from file"""
|
|
|
|
|
|
|
|
|
|
| 50 |
try:
|
| 51 |
+
with open(self.users_file, 'r') as f:
|
| 52 |
+
return json.load(f)
|
| 53 |
+
except:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
return {}
|
| 55 |
|
| 56 |
def save_users(self, users: Dict[str, Any]):
|
| 57 |
+
"""Save users to file"""
|
| 58 |
+
with open(self.users_file, 'w') as f:
|
| 59 |
+
json.dump(users, f, indent=2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
def create_user(self, username: str, email: str, password: str) -> Dict[str, Any]:
|
| 62 |
"""Create a new user"""
|
|
|
|
| 84 |
return {'success': True, 'user_id': user_id}
|
| 85 |
|
| 86 |
def authenticate_user(self, username: str, password: str) -> Dict[str, Any]:
|
| 87 |
+
"""Authenticate user credentials"""
|
|
|
|
|
|
|
| 88 |
users = self.load_users()
|
|
|
|
| 89 |
|
| 90 |
if username not in users:
|
|
|
|
|
|
|
| 91 |
return {'success': False, 'error': 'Invalid username or password'}
|
| 92 |
|
| 93 |
user = users[username]
|
|
|
|
| 94 |
if not self.verify_password(password, user['password_hash']):
|
|
|
|
| 95 |
return {'success': False, 'error': 'Invalid username or password'}
|
| 96 |
|
| 97 |
if not user.get('is_active', True):
|
|
|
|
| 98 |
return {'success': False, 'error': 'Account is disabled'}
|
| 99 |
|
| 100 |
+
# Generate JWT token with shorter expiration for security
|
| 101 |
+
token = jwt.encode({
|
| 102 |
+
'user_id': user['user_id'],
|
| 103 |
+
'username': username,
|
| 104 |
+
'exp': datetime.utcnow() + timedelta(hours=8) # 8 hours instead of 7 days
|
| 105 |
+
}, self.secret_key, algorithm='HS256')
|
| 106 |
+
|
| 107 |
+
# Track active session
|
| 108 |
+
self.add_active_session(user['user_id'], token)
|
| 109 |
+
|
| 110 |
+
return {
|
| 111 |
+
'success': True,
|
| 112 |
+
'token': token,
|
| 113 |
+
'user_id': user['user_id'],
|
| 114 |
+
'username': username
|
| 115 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
def verify_token(self, token: str) -> Optional[Dict[str, Any]]:
|
| 118 |
+
"""Verify JWT token and check active session"""
|
| 119 |
try:
|
| 120 |
payload = jwt.decode(token, self.secret_key, algorithms=['HS256'])
|
| 121 |
user_id = payload.get('user_id')
|
|
|
|
| 126 |
|
| 127 |
# Update session activity
|
| 128 |
self.update_session_activity(user_id)
|
| 129 |
+
|
| 130 |
return payload
|
| 131 |
except jwt.ExpiredSignatureError:
|
|
|
|
| 132 |
return None
|
| 133 |
except jwt.InvalidTokenError:
|
|
|
|
| 134 |
return None
|
| 135 |
+
|
| 136 |
+
def get_current_user(self, request_obj) -> Optional[Dict[str, Any]]:
|
| 137 |
+
"""Get current user from request"""
|
| 138 |
+
if not FLASK_AVAILABLE or not request_obj:
|
| 139 |
return None
|
| 140 |
+
|
| 141 |
+
# Try Authorization header first
|
| 142 |
+
auth_header = request_obj.headers.get('Authorization')
|
| 143 |
+
if auth_header and auth_header.startswith('Bearer '):
|
| 144 |
+
token = auth_header.split(' ')[1]
|
| 145 |
+
return self.verify_token(token)
|
| 146 |
+
|
| 147 |
+
# Try session
|
| 148 |
+
if session:
|
| 149 |
+
token = session.get('auth_token')
|
| 150 |
+
if token:
|
| 151 |
+
return self.verify_token(token)
|
| 152 |
+
|
| 153 |
+
return None
|
| 154 |
|
| 155 |
def create_default_admin(self) -> Dict[str, Any]:
|
| 156 |
"""Create default admin user if it doesn't exist"""
|
|
|
|
| 159 |
admin_username = "admin"
|
| 160 |
admin_user_id = "admin_user"
|
| 161 |
|
| 162 |
+
# Check if admin already exists (by username or user_id)
|
| 163 |
if admin_username in users:
|
| 164 |
return {'success': True, 'message': 'Admin user already exists'}
|
| 165 |
|
| 166 |
+
# Check if user_id already exists
|
| 167 |
+
for user_data in users.values():
|
| 168 |
+
if user_data.get('user_id') == admin_user_id:
|
| 169 |
+
return {'success': True, 'message': 'Admin user ID already exists'}
|
| 170 |
+
|
| 171 |
# Create admin user
|
| 172 |
+
users[admin_username] = {
|
| 173 |
+
'user_id': admin_user_id,
|
| 174 |
+
'email': '[email protected]',
|
| 175 |
+
'password_hash': self.hash_password('admin123'), # Default password
|
| 176 |
+
'created_at': datetime.now().isoformat(),
|
| 177 |
+
'is_active': True,
|
| 178 |
+
'is_admin': True
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
self.save_users(users)
|
| 182 |
+
return {
|
| 183 |
+
'success': True,
|
| 184 |
+
'message': 'Default admin user created',
|
| 185 |
+
'username': admin_username,
|
| 186 |
+
'password': 'admin123',
|
| 187 |
+
'note': 'Please change the default password after first login'
|
| 188 |
+
}
|
|
|
|
|
|
|
|
|
|
| 189 |
|
| 190 |
def load_active_sessions(self) -> Dict[str, Any]:
|
| 191 |
+
"""Load active sessions from file"""
|
|
|
|
|
|
|
|
|
|
| 192 |
try:
|
| 193 |
+
if os.path.exists(self.session_file):
|
| 194 |
with open(self.session_file, 'r') as f:
|
| 195 |
return json.load(f)
|
| 196 |
+
except:
|
| 197 |
+
pass
|
| 198 |
return {}
|
| 199 |
|
| 200 |
def save_active_sessions(self, sessions: Dict[str, Any]):
|
| 201 |
+
"""Save active sessions to file"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
try:
|
| 203 |
with open(self.session_file, 'w') as f:
|
| 204 |
json.dump(sessions, f, indent=2)
|
| 205 |
+
except:
|
| 206 |
+
pass
|
| 207 |
|
| 208 |
def add_active_session(self, user_id: str, token: str):
|
| 209 |
"""Add an active session"""
|
|
|
|
| 215 |
}
|
| 216 |
self.save_active_sessions(sessions)
|
| 217 |
|
| 218 |
+
def remove_active_session(self, user_id: str):
|
| 219 |
+
"""Remove an active session"""
|
| 220 |
+
sessions = self.load_active_sessions()
|
| 221 |
+
if user_id in sessions:
|
| 222 |
+
del sessions[user_id]
|
| 223 |
+
self.save_active_sessions(sessions)
|
| 224 |
+
|
| 225 |
def is_session_active(self, user_id: str, token: str) -> bool:
|
| 226 |
"""Check if a session is active"""
|
| 227 |
sessions = self.load_active_sessions()
|
|
|
|
| 232 |
if session.get('token') != token:
|
| 233 |
return False
|
| 234 |
|
| 235 |
+
# Check if session is expired (8 hours)
|
| 236 |
+
created_at = datetime.fromisoformat(session['created_at'])
|
| 237 |
+
if datetime.now() - created_at > timedelta(hours=8):
|
| 238 |
+
self.remove_active_session(user_id)
|
|
|
|
|
|
|
|
|
|
| 239 |
return False
|
| 240 |
|
| 241 |
return True
|
| 242 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
def logout_user(self, user_id: str):
|
| 244 |
"""Logout user and invalidate session"""
|
| 245 |
self.remove_active_session(user_id)
|
|
|
|
| 252 |
|
| 253 |
expired_sessions = []
|
| 254 |
for user_id, session in sessions.items():
|
| 255 |
+
created_at = datetime.fromisoformat(session['created_at'])
|
| 256 |
+
if current_time - created_at > timedelta(hours=8):
|
|
|
|
|
|
|
|
|
|
| 257 |
expired_sessions.append(user_id)
|
| 258 |
|
| 259 |
for user_id in expired_sessions:
|
|
|
|
| 264 |
|
| 265 |
return len(expired_sessions)
|
| 266 |
|
| 267 |
+
def update_session_activity(self, user_id: str):
|
| 268 |
+
"""Update last activity time for a session"""
|
|
|
|
| 269 |
sessions = self.load_active_sessions()
|
| 270 |
+
if user_id in sessions:
|
| 271 |
+
sessions[user_id]['last_activity'] = datetime.now().isoformat()
|
| 272 |
+
self.save_active_sessions(sessions)
|
| 273 |
+
|
| 274 |
+
# Global auth manager
|
| 275 |
+
auth_manager = AuthManager()
|
| 276 |
+
|
| 277 |
+
def require_auth(f):
|
| 278 |
+
"""Decorator to require authentication"""
|
| 279 |
+
@wraps(f)
|
| 280 |
+
def decorated_function(*args, **kwargs):
|
| 281 |
+
if not FLASK_AVAILABLE:
|
| 282 |
+
return f(*args, **kwargs)
|
| 283 |
+
|
| 284 |
+
user = auth_manager.get_current_user(request)
|
| 285 |
+
if not user:
|
| 286 |
+
if request.is_json:
|
| 287 |
+
return jsonify({'success': False, 'error': 'Authentication required'}), 401
|
| 288 |
+
else:
|
| 289 |
+
return redirect(url_for('login'))
|
| 290 |
+
return f(*args, **kwargs)
|
| 291 |
+
return decorated_function
|
| 292 |
|
| 293 |
+
def get_current_user() -> Optional[Dict[str, Any]]:
|
| 294 |
+
"""Get current authenticated user"""
|
| 295 |
+
if not FLASK_AVAILABLE:
|
| 296 |
+
return None
|
| 297 |
+
return auth_manager.get_current_user(request)
|
src/scripts/dev_server.py
CHANGED
|
@@ -24,7 +24,7 @@ import socket
|
|
| 24 |
sys.path.append(str(Path(__file__).parent.parent.parent))
|
| 25 |
|
| 26 |
# Import the main app from main.py
|
| 27 |
-
from
|
| 28 |
|
| 29 |
# Setup logging
|
| 30 |
logging.basicConfig(
|
|
@@ -104,7 +104,7 @@ Auto-reload enabled for Python files
|
|
| 104 |
"""Check if virtual environment exists"""
|
| 105 |
# Since we're importing directly, just check if we can import the modules
|
| 106 |
try:
|
| 107 |
-
import
|
| 108 |
logger.info("Successfully imported main application")
|
| 109 |
return True
|
| 110 |
except ImportError as e:
|
|
|
|
| 24 |
sys.path.append(str(Path(__file__).parent.parent.parent))
|
| 25 |
|
| 26 |
# Import the main app from main.py
|
| 27 |
+
from ResearchMate.app import app
|
| 28 |
|
| 29 |
# Setup logging
|
| 30 |
logging.basicConfig(
|
|
|
|
| 104 |
"""Check if virtual environment exists"""
|
| 105 |
# Since we're importing directly, just check if we can import the modules
|
| 106 |
try:
|
| 107 |
+
import ResearchMate.app as app
|
| 108 |
logger.info("Successfully imported main application")
|
| 109 |
return True
|
| 110 |
except ImportError as e:
|
src/static/js/main.js
CHANGED
|
@@ -3,62 +3,182 @@
|
|
| 3 |
// Global variables
|
| 4 |
let currentToast = null;
|
| 5 |
|
| 6 |
-
// Authentication
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
} else {
|
| 23 |
-
//
|
| 24 |
-
|
| 25 |
-
window.location.href = '/login';
|
| 26 |
-
}
|
| 27 |
-
}
|
| 28 |
-
})
|
| 29 |
-
.catch((error) => {
|
| 30 |
-
console.error('User status check error:', error);
|
| 31 |
-
if (window.location.pathname !== '/login') {
|
| 32 |
-
window.location.href = '/login';
|
| 33 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
// Initialize tooltips
|
| 39 |
initializeTooltips();
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
// Handle beforeunload event (browser/tab closing)
|
| 42 |
window.addEventListener('beforeunload', function() {
|
| 43 |
// Clear sessionStorage on page unload (but keep localStorage for potential restoration)
|
| 44 |
sessionStorage.clear();
|
| 45 |
});
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
// Initialize smooth scrolling
|
| 48 |
initializeSmoothScrolling();
|
| 49 |
-
|
| 50 |
// Initialize animations
|
| 51 |
initializeAnimations();
|
| 52 |
-
|
| 53 |
// Initialize keyboard shortcuts
|
| 54 |
initializeKeyboardShortcuts();
|
| 55 |
-
|
|
|
|
|
|
|
| 56 |
// Initialize upload
|
| 57 |
initializeUpload();
|
| 58 |
-
|
| 59 |
// Initialize search page (if on search page)
|
| 60 |
initializeSearchPage();
|
| 61 |
-
|
| 62 |
console.log('ResearchMate initialized successfully!');
|
| 63 |
});
|
| 64 |
|
|
@@ -72,8 +192,11 @@ function initializeTooltips() {
|
|
| 72 |
function initializeSmoothScrolling() {
|
| 73 |
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
| 74 |
anchor.addEventListener('click', function (e) {
|
|
|
|
|
|
|
|
|
|
| 75 |
e.preventDefault();
|
| 76 |
-
const target = document.querySelector(
|
| 77 |
if (target) {
|
| 78 |
target.scrollIntoView({
|
| 79 |
behavior: 'smooth',
|
|
@@ -139,35 +262,35 @@ function initializeKeyboardShortcuts() {
|
|
| 139 |
// Theme toggle removed: always use dark theme
|
| 140 |
|
| 141 |
// Enhanced Upload functionality
|
| 142 |
-
|
| 143 |
const uploadArea = document.getElementById('upload-area');
|
| 144 |
const fileInput = document.getElementById('pdf-file');
|
| 145 |
const uploadBtn = document.getElementById('upload-btn');
|
| 146 |
-
|
| 147 |
if (!uploadArea || !fileInput || !uploadBtn) return;
|
| 148 |
-
|
| 149 |
// Restore previous upload results if they exist
|
| 150 |
-
|
| 151 |
-
|
| 152 |
// Click to browse files
|
| 153 |
uploadArea.addEventListener('click', () => {
|
| 154 |
fileInput.click();
|
| 155 |
});
|
| 156 |
-
|
| 157 |
// Drag and drop functionality
|
| 158 |
uploadArea.addEventListener('dragover', (e) => {
|
| 159 |
e.preventDefault();
|
| 160 |
uploadArea.classList.add('dragover');
|
| 161 |
});
|
| 162 |
-
|
| 163 |
uploadArea.addEventListener('dragleave', () => {
|
| 164 |
uploadArea.classList.remove('dragover');
|
| 165 |
});
|
| 166 |
-
|
| 167 |
uploadArea.addEventListener('drop', (e) => {
|
| 168 |
e.preventDefault();
|
| 169 |
uploadArea.classList.remove('dragover');
|
| 170 |
-
|
| 171 |
const files = e.dataTransfer.files;
|
| 172 |
if (files.length > 0 && files[0].type === 'application/pdf') {
|
| 173 |
fileInput.files = files;
|
|
@@ -176,18 +299,18 @@ async function initializeUpload() {
|
|
| 176 |
showToast('Please select a valid PDF file', 'danger');
|
| 177 |
}
|
| 178 |
});
|
| 179 |
-
|
| 180 |
// File input change
|
| 181 |
fileInput.addEventListener('change', (e) => {
|
| 182 |
if (e.target.files.length > 0) {
|
| 183 |
handleFileSelection(e.target.files[0]);
|
| 184 |
}
|
| 185 |
});
|
| 186 |
-
|
| 187 |
function handleFileSelection(file) {
|
| 188 |
uploadBtn.disabled = false;
|
| 189 |
uploadBtn.innerHTML = `<i class="fas fa-upload me-2"></i>Upload "${file.name}"`;
|
| 190 |
-
|
| 191 |
// Update upload area
|
| 192 |
uploadArea.innerHTML = `
|
| 193 |
<i class="fas fa-file-pdf text-danger"></i>
|
|
@@ -215,36 +338,43 @@ function toggleUploadArea() {
|
|
| 215 |
}
|
| 216 |
|
| 217 |
// Upload result persistence functions
|
| 218 |
-
|
| 219 |
try {
|
| 220 |
-
const currentUser =
|
| 221 |
-
|
| 222 |
-
|
|
|
|
|
|
|
| 223 |
return;
|
| 224 |
}
|
|
|
|
| 225 |
const dataToSave = {
|
| 226 |
...data,
|
| 227 |
userId: currentUser,
|
|
|
|
| 228 |
savedAt: new Date().toISOString(),
|
| 229 |
pageUrl: window.location.pathname
|
| 230 |
};
|
|
|
|
| 231 |
saveToLocalStorage('researchmate_upload_results', dataToSave);
|
| 232 |
} catch (error) {
|
| 233 |
console.error('Failed to save upload results:', error);
|
| 234 |
}
|
| 235 |
}
|
| 236 |
|
| 237 |
-
|
| 238 |
try {
|
| 239 |
const resultsContainer = document.getElementById('results-container');
|
| 240 |
if (!resultsContainer) return;
|
| 241 |
-
|
| 242 |
-
|
|
|
|
| 243 |
if (!currentUser) {
|
| 244 |
// No user logged in, clear any existing results
|
| 245 |
clearUploadResults();
|
| 246 |
return;
|
| 247 |
}
|
|
|
|
| 248 |
const savedData = loadFromLocalStorage('researchmate_upload_results');
|
| 249 |
if (savedData && savedData.pageUrl === window.location.pathname) {
|
| 250 |
// Check if data belongs to current user
|
|
@@ -253,12 +383,22 @@ async function restoreUploadResults() {
|
|
| 253 |
clearUploadResults();
|
| 254 |
return;
|
| 255 |
}
|
| 256 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
const savedTime = new Date(savedData.savedAt);
|
| 258 |
const now = new Date();
|
| 259 |
const hoursDiff = (now - savedTime) / (1000 * 60 * 60);
|
|
|
|
| 260 |
if (hoursDiff < 1) {
|
| 261 |
-
console.log('Restoring upload results
|
| 262 |
displayUploadResults(savedData);
|
| 263 |
showToast('Previous PDF analysis restored', 'info', 3000);
|
| 264 |
} else {
|
|
@@ -271,14 +411,15 @@ async function restoreUploadResults() {
|
|
| 271 |
}
|
| 272 |
}
|
| 273 |
|
| 274 |
-
// Helper function to get current user ID
|
| 275 |
-
|
| 276 |
try {
|
| 277 |
-
const
|
| 278 |
-
if (!
|
| 279 |
-
|
| 280 |
-
//
|
| 281 |
-
|
|
|
|
| 282 |
} catch (error) {
|
| 283 |
console.error('Failed to get current user ID:', error);
|
| 284 |
return null;
|
|
@@ -1056,18 +1197,22 @@ function loadFromLocalStorage(key, defaultValue = null) {
|
|
| 1056 |
|
| 1057 |
// Enhanced logout function with security cleanup
|
| 1058 |
function logout() {
|
|
|
|
|
|
|
|
|
|
| 1059 |
// Clear all session data
|
| 1060 |
sessionStorage.clear();
|
| 1061 |
-
|
| 1062 |
-
localStorage
|
| 1063 |
-
|
|
|
|
|
|
|
| 1064 |
// Call logout API
|
| 1065 |
fetch('/api/auth/logout', {
|
| 1066 |
method: 'POST',
|
| 1067 |
headers: {
|
| 1068 |
'Content-Type': 'application/json',
|
| 1069 |
-
}
|
| 1070 |
-
credentials: 'include'
|
| 1071 |
})
|
| 1072 |
.then(() => {
|
| 1073 |
// Redirect to login page
|
|
@@ -1082,7 +1227,8 @@ function logout() {
|
|
| 1082 |
// Make logout function globally available
|
| 1083 |
window.logout = logout;
|
| 1084 |
|
| 1085 |
-
|
|
|
|
| 1086 |
|
| 1087 |
// Export functions for global use
|
| 1088 |
window.ResearchMate = {
|
|
@@ -1104,8 +1250,7 @@ window.ResearchMate = {
|
|
| 1104 |
saveUploadResults,
|
| 1105 |
restoreUploadResults,
|
| 1106 |
clearUploadResults,
|
| 1107 |
-
displayUploadResults
|
| 1108 |
-
getCurrentUserId
|
| 1109 |
};
|
| 1110 |
|
| 1111 |
// Make clearUploadResults globally available for onclick handlers
|
|
|
|
| 3 |
// Global variables
|
| 4 |
let currentToast = null;
|
| 5 |
|
| 6 |
+
// Authentication utilities with enhanced security
|
| 7 |
+
let sessionTimeout = null;
|
| 8 |
+
let lastActivityTime = Date.now();
|
| 9 |
+
const SESSION_TIMEOUT_MINUTES = 480; // 8 hours for prototype (less aggressive)
|
| 10 |
+
const ACTIVITY_CHECK_INTERVAL = 300000; // Check every 5 minutes
|
| 11 |
+
|
| 12 |
+
function getAuthToken() {
|
| 13 |
+
// Check both sessionStorage (preferred) and localStorage (fallback)
|
| 14 |
+
return sessionStorage.getItem('authToken') || localStorage.getItem('authToken');
|
| 15 |
+
}
|
| 16 |
|
| 17 |
+
function setAuthToken(token) {
|
| 18 |
+
// Store in sessionStorage for better security (clears on browser close)
|
| 19 |
+
sessionStorage.setItem('authToken', token);
|
| 20 |
+
// Also store in localStorage for compatibility, but with shorter expiry
|
| 21 |
+
localStorage.setItem('authToken', token);
|
| 22 |
+
localStorage.setItem('tokenTimestamp', Date.now().toString());
|
| 23 |
+
|
| 24 |
+
// Set cookie with HttpOnly equivalent behavior
|
| 25 |
+
document.cookie = `authToken=${token}; path=/; SameSite=Strict; Secure=${location.protocol === 'https:'}`;
|
| 26 |
+
|
| 27 |
+
// Reset activity tracking
|
| 28 |
+
lastActivityTime = Date.now();
|
| 29 |
+
startSessionTimeout();
|
| 30 |
+
}
|
| 31 |
|
| 32 |
+
function clearAuthToken() {
|
| 33 |
+
sessionStorage.removeItem('authToken');
|
| 34 |
+
sessionStorage.removeItem('userId');
|
| 35 |
+
localStorage.removeItem('authToken');
|
| 36 |
+
localStorage.removeItem('userId');
|
| 37 |
+
localStorage.removeItem('tokenTimestamp');
|
| 38 |
+
|
| 39 |
+
// Clear cookie
|
| 40 |
+
document.cookie = 'authToken=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Strict';
|
| 41 |
+
|
| 42 |
+
clearTimeout(sessionTimeout);
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
function isTokenExpired() {
|
| 46 |
+
const timestamp = localStorage.getItem('tokenTimestamp');
|
| 47 |
+
if (!timestamp) return true;
|
| 48 |
+
|
| 49 |
+
const tokenAge = Date.now() - parseInt(timestamp);
|
| 50 |
+
const maxAge = 24 * 60 * 60 * 1000; // 24 hours
|
| 51 |
+
|
| 52 |
+
return tokenAge > maxAge;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
function startSessionTimeout() {
|
| 56 |
+
clearTimeout(sessionTimeout);
|
| 57 |
+
sessionTimeout = setTimeout(() => {
|
| 58 |
+
const inactivityTime = Date.now() - lastActivityTime;
|
| 59 |
+
if (inactivityTime >= SESSION_TIMEOUT_MINUTES * 60 * 1000) {
|
| 60 |
+
// Session expired due to inactivity
|
| 61 |
+
showToast('Session expired due to inactivity. Please log in again.', 'warning');
|
| 62 |
+
logout();
|
| 63 |
} else {
|
| 64 |
+
// Still active, reset timer
|
| 65 |
+
startSessionTimeout();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
}
|
| 67 |
+
}, ACTIVITY_CHECK_INTERVAL);
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
function trackActivity() {
|
| 71 |
+
lastActivityTime = Date.now();
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
function setAuthHeaders(headers = {}) {
|
| 75 |
+
const token = getAuthToken();
|
| 76 |
+
if (token && !isTokenExpired()) {
|
| 77 |
+
headers['Authorization'] = `Bearer ${token}`;
|
| 78 |
+
}
|
| 79 |
+
return headers;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
function makeAuthenticatedRequest(url, options = {}) {
|
| 83 |
+
const headers = setAuthHeaders(options.headers || {});
|
| 84 |
+
return fetch(url, {
|
| 85 |
+
...options,
|
| 86 |
+
headers: headers
|
| 87 |
});
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
// Check if user is authenticated
|
| 91 |
+
function isAuthenticated() {
|
| 92 |
+
const token = getAuthToken();
|
| 93 |
+
return !!(token && !isTokenExpired());
|
| 94 |
+
}
|
| 95 |
|
| 96 |
+
// Redirect to login if not authenticated
|
| 97 |
+
function requireAuth() {
|
| 98 |
+
if (!isAuthenticated()) {
|
| 99 |
+
clearAuthToken();
|
| 100 |
+
window.location.href = '/login';
|
| 101 |
+
return false;
|
| 102 |
+
}
|
| 103 |
+
return true;
|
| 104 |
+
}
|
| 105 |
|
| 106 |
+
// Document ready with enhanced security
|
| 107 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 108 |
+
// Check authentication on protected pages
|
| 109 |
+
if (window.location.pathname !== '/login' && !isAuthenticated()) {
|
| 110 |
+
clearAuthToken();
|
| 111 |
+
window.location.href = '/login';
|
| 112 |
+
return;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
// Start session timeout if authenticated
|
| 116 |
+
if (isAuthenticated()) {
|
| 117 |
+
startSessionTimeout();
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
// Track user activity for session timeout
|
| 121 |
+
document.addEventListener('click', trackActivity);
|
| 122 |
+
document.addEventListener('keypress', trackActivity);
|
| 123 |
+
document.addEventListener('scroll', trackActivity);
|
| 124 |
+
document.addEventListener('mousemove', trackActivity);
|
| 125 |
+
|
| 126 |
// Initialize tooltips
|
| 127 |
initializeTooltips();
|
| 128 |
+
|
| 129 |
+
// Handle page visibility changes (user switches tabs or minimizes browser)
|
| 130 |
+
document.addEventListener('visibilitychange', function() {
|
| 131 |
+
if (document.hidden) {
|
| 132 |
+
// Page is hidden, reduce activity tracking
|
| 133 |
+
clearTimeout(sessionTimeout);
|
| 134 |
+
} else {
|
| 135 |
+
// Page is visible again, resume activity tracking
|
| 136 |
+
if (isAuthenticated()) {
|
| 137 |
+
trackActivity();
|
| 138 |
+
startSessionTimeout();
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
});
|
| 142 |
+
|
| 143 |
// Handle beforeunload event (browser/tab closing)
|
| 144 |
window.addEventListener('beforeunload', function() {
|
| 145 |
// Clear sessionStorage on page unload (but keep localStorage for potential restoration)
|
| 146 |
sessionStorage.clear();
|
| 147 |
});
|
| 148 |
+
|
| 149 |
+
// Periodically validate token with server (disabled for prototype)
|
| 150 |
+
// if (isAuthenticated()) {
|
| 151 |
+
// setInterval(async function() {
|
| 152 |
+
// try {
|
| 153 |
+
// const response = await makeAuthenticatedRequest('/api/user/status');
|
| 154 |
+
// if (!response.ok) {
|
| 155 |
+
// // Token is invalid or expired
|
| 156 |
+
// showToast('Session expired. Please log in again.', 'warning');
|
| 157 |
+
// logout();
|
| 158 |
+
// }
|
| 159 |
+
// } catch (error) {
|
| 160 |
+
// console.log('Token validation failed:', error);
|
| 161 |
+
// }
|
| 162 |
+
// }, 5 * 60 * 1000); // Check every 5 minutes
|
| 163 |
+
// }
|
| 164 |
+
|
| 165 |
// Initialize smooth scrolling
|
| 166 |
initializeSmoothScrolling();
|
| 167 |
+
|
| 168 |
// Initialize animations
|
| 169 |
initializeAnimations();
|
| 170 |
+
|
| 171 |
// Initialize keyboard shortcuts
|
| 172 |
initializeKeyboardShortcuts();
|
| 173 |
+
|
| 174 |
+
// Theme toggle removed
|
| 175 |
+
|
| 176 |
// Initialize upload
|
| 177 |
initializeUpload();
|
| 178 |
+
|
| 179 |
// Initialize search page (if on search page)
|
| 180 |
initializeSearchPage();
|
| 181 |
+
|
| 182 |
console.log('ResearchMate initialized successfully!');
|
| 183 |
});
|
| 184 |
|
|
|
|
| 192 |
function initializeSmoothScrolling() {
|
| 193 |
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
| 194 |
anchor.addEventListener('click', function (e) {
|
| 195 |
+
const href = this.getAttribute('href');
|
| 196 |
+
// Skip if href is just '#', which is not a valid selector
|
| 197 |
+
if (href === '#') return;
|
| 198 |
e.preventDefault();
|
| 199 |
+
const target = document.querySelector(href);
|
| 200 |
if (target) {
|
| 201 |
target.scrollIntoView({
|
| 202 |
behavior: 'smooth',
|
|
|
|
| 262 |
// Theme toggle removed: always use dark theme
|
| 263 |
|
| 264 |
// Enhanced Upload functionality
|
| 265 |
+
function initializeUpload() {
|
| 266 |
const uploadArea = document.getElementById('upload-area');
|
| 267 |
const fileInput = document.getElementById('pdf-file');
|
| 268 |
const uploadBtn = document.getElementById('upload-btn');
|
| 269 |
+
|
| 270 |
if (!uploadArea || !fileInput || !uploadBtn) return;
|
| 271 |
+
|
| 272 |
// Restore previous upload results if they exist
|
| 273 |
+
restoreUploadResults();
|
| 274 |
+
|
| 275 |
// Click to browse files
|
| 276 |
uploadArea.addEventListener('click', () => {
|
| 277 |
fileInput.click();
|
| 278 |
});
|
| 279 |
+
|
| 280 |
// Drag and drop functionality
|
| 281 |
uploadArea.addEventListener('dragover', (e) => {
|
| 282 |
e.preventDefault();
|
| 283 |
uploadArea.classList.add('dragover');
|
| 284 |
});
|
| 285 |
+
|
| 286 |
uploadArea.addEventListener('dragleave', () => {
|
| 287 |
uploadArea.classList.remove('dragover');
|
| 288 |
});
|
| 289 |
+
|
| 290 |
uploadArea.addEventListener('drop', (e) => {
|
| 291 |
e.preventDefault();
|
| 292 |
uploadArea.classList.remove('dragover');
|
| 293 |
+
|
| 294 |
const files = e.dataTransfer.files;
|
| 295 |
if (files.length > 0 && files[0].type === 'application/pdf') {
|
| 296 |
fileInput.files = files;
|
|
|
|
| 299 |
showToast('Please select a valid PDF file', 'danger');
|
| 300 |
}
|
| 301 |
});
|
| 302 |
+
|
| 303 |
// File input change
|
| 304 |
fileInput.addEventListener('change', (e) => {
|
| 305 |
if (e.target.files.length > 0) {
|
| 306 |
handleFileSelection(e.target.files[0]);
|
| 307 |
}
|
| 308 |
});
|
| 309 |
+
|
| 310 |
function handleFileSelection(file) {
|
| 311 |
uploadBtn.disabled = false;
|
| 312 |
uploadBtn.innerHTML = `<i class="fas fa-upload me-2"></i>Upload "${file.name}"`;
|
| 313 |
+
|
| 314 |
// Update upload area
|
| 315 |
uploadArea.innerHTML = `
|
| 316 |
<i class="fas fa-file-pdf text-danger"></i>
|
|
|
|
| 338 |
}
|
| 339 |
|
| 340 |
// Upload result persistence functions
|
| 341 |
+
function saveUploadResults(data) {
|
| 342 |
try {
|
| 343 |
+
const currentUser = getCurrentUserId();
|
| 344 |
+
const currentSession = getSessionId();
|
| 345 |
+
|
| 346 |
+
if (!currentUser || !currentSession) {
|
| 347 |
+
console.warn('Cannot save upload results: no user or session');
|
| 348 |
return;
|
| 349 |
}
|
| 350 |
+
|
| 351 |
const dataToSave = {
|
| 352 |
...data,
|
| 353 |
userId: currentUser,
|
| 354 |
+
sessionId: currentSession,
|
| 355 |
savedAt: new Date().toISOString(),
|
| 356 |
pageUrl: window.location.pathname
|
| 357 |
};
|
| 358 |
+
|
| 359 |
saveToLocalStorage('researchmate_upload_results', dataToSave);
|
| 360 |
} catch (error) {
|
| 361 |
console.error('Failed to save upload results:', error);
|
| 362 |
}
|
| 363 |
}
|
| 364 |
|
| 365 |
+
function restoreUploadResults() {
|
| 366 |
try {
|
| 367 |
const resultsContainer = document.getElementById('results-container');
|
| 368 |
if (!resultsContainer) return;
|
| 369 |
+
|
| 370 |
+
// Get current user from session/token
|
| 371 |
+
const currentUser = getCurrentUserId(); // You'll need to implement this
|
| 372 |
if (!currentUser) {
|
| 373 |
// No user logged in, clear any existing results
|
| 374 |
clearUploadResults();
|
| 375 |
return;
|
| 376 |
}
|
| 377 |
+
|
| 378 |
const savedData = loadFromLocalStorage('researchmate_upload_results');
|
| 379 |
if (savedData && savedData.pageUrl === window.location.pathname) {
|
| 380 |
// Check if data belongs to current user
|
|
|
|
| 383 |
clearUploadResults();
|
| 384 |
return;
|
| 385 |
}
|
| 386 |
+
|
| 387 |
+
// Check if data is from current session
|
| 388 |
+
const currentSessionId = getSessionId(); // You'll need to implement this
|
| 389 |
+
if (savedData.sessionId !== currentSessionId) {
|
| 390 |
+
console.log('Upload results from different session, clearing');
|
| 391 |
+
clearUploadResults();
|
| 392 |
+
return;
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
// Check if data is recent (within current session, max 1 hour)
|
| 396 |
const savedTime = new Date(savedData.savedAt);
|
| 397 |
const now = new Date();
|
| 398 |
const hoursDiff = (now - savedTime) / (1000 * 60 * 60);
|
| 399 |
+
|
| 400 |
if (hoursDiff < 1) {
|
| 401 |
+
console.log('Restoring upload results from current session');
|
| 402 |
displayUploadResults(savedData);
|
| 403 |
showToast('Previous PDF analysis restored', 'info', 3000);
|
| 404 |
} else {
|
|
|
|
| 411 |
}
|
| 412 |
}
|
| 413 |
|
| 414 |
+
// Helper function to get current user ID
|
| 415 |
+
function getCurrentUserId() {
|
| 416 |
try {
|
| 417 |
+
const token = localStorage.getItem('authToken') || sessionStorage.getItem('authToken');
|
| 418 |
+
if (!token) return null;
|
| 419 |
+
|
| 420 |
+
// Decode JWT token to get user ID (simple base64 decode)
|
| 421 |
+
const payload = JSON.parse(atob(token.split('.')[1]));
|
| 422 |
+
return payload.user_id || payload.sub;
|
| 423 |
} catch (error) {
|
| 424 |
console.error('Failed to get current user ID:', error);
|
| 425 |
return null;
|
|
|
|
| 1197 |
|
| 1198 |
// Enhanced logout function with security cleanup
|
| 1199 |
function logout() {
|
| 1200 |
+
// Clear all authentication data
|
| 1201 |
+
clearAuthToken();
|
| 1202 |
+
|
| 1203 |
// Clear all session data
|
| 1204 |
sessionStorage.clear();
|
| 1205 |
+
|
| 1206 |
+
// Clear specific localStorage items but keep non-sensitive data
|
| 1207 |
+
const keysToRemove = ['authToken', 'userId', 'tokenTimestamp', 'userSession'];
|
| 1208 |
+
keysToRemove.forEach(key => localStorage.removeItem(key));
|
| 1209 |
+
|
| 1210 |
// Call logout API
|
| 1211 |
fetch('/api/auth/logout', {
|
| 1212 |
method: 'POST',
|
| 1213 |
headers: {
|
| 1214 |
'Content-Type': 'application/json',
|
| 1215 |
+
}
|
|
|
|
| 1216 |
})
|
| 1217 |
.then(() => {
|
| 1218 |
// Redirect to login page
|
|
|
|
| 1227 |
// Make logout function globally available
|
| 1228 |
window.logout = logout;
|
| 1229 |
|
| 1230 |
+
// Make makeAuthenticatedRequest globally available
|
| 1231 |
+
window.makeAuthenticatedRequest = makeAuthenticatedRequest;
|
| 1232 |
|
| 1233 |
// Export functions for global use
|
| 1234 |
window.ResearchMate = {
|
|
|
|
| 1250 |
saveUploadResults,
|
| 1251 |
restoreUploadResults,
|
| 1252 |
clearUploadResults,
|
| 1253 |
+
displayUploadResults
|
|
|
|
| 1254 |
};
|
| 1255 |
|
| 1256 |
// Make clearUploadResults globally available for onclick handlers
|
src/templates/login.html
CHANGED
|
@@ -12,10 +12,6 @@
|
|
| 12 |
<h3 class="text-primary-custom">Welcome to ResearchMate</h3>
|
| 13 |
<p class="text-muted">Please log in to access your research projects</p>
|
| 14 |
</div>
|
| 15 |
-
|
| 16 |
-
<!-- Alert container for messages -->
|
| 17 |
-
<div id="alert-container"></div>
|
| 18 |
-
|
| 19 |
<form id="login-form">
|
| 20 |
<div class="mb-3">
|
| 21 |
<label for="username" class="form-label">Username</label>
|
|
@@ -26,7 +22,7 @@
|
|
| 26 |
<input type="password" class="form-control" id="password" name="password" required>
|
| 27 |
</div>
|
| 28 |
<div class="d-grid">
|
| 29 |
-
<button type="submit" class="btn btn-primary"
|
| 30 |
<i class="fas fa-sign-in-alt me-2"></i>Login
|
| 31 |
</button>
|
| 32 |
</div>
|
|
@@ -56,19 +52,19 @@
|
|
| 56 |
<form id="register-form">
|
| 57 |
<div class="mb-3">
|
| 58 |
<label for="reg-username" class="form-label">Username</label>
|
| 59 |
-
|
| 60 |
</div>
|
| 61 |
<div class="mb-3">
|
| 62 |
<label for="reg-email" class="form-label">Email</label>
|
| 63 |
-
|
| 64 |
</div>
|
| 65 |
<div class="mb-3">
|
| 66 |
<label for="reg-password" class="form-label">Password</label>
|
| 67 |
-
|
| 68 |
</div>
|
| 69 |
<div class="mb-3">
|
| 70 |
<label for="reg-confirm-password" class="form-label">Confirm Password</label>
|
| 71 |
-
|
| 72 |
</div>
|
| 73 |
</form>
|
| 74 |
</div>
|
|
@@ -85,215 +81,129 @@
|
|
| 85 |
|
| 86 |
{% block extra_js %}
|
| 87 |
<script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
document.addEventListener('DOMContentLoaded', function() {
|
| 89 |
-
// Always verify authentication with backend on page load
|
| 90 |
-
fetch('/api/user/status', {
|
| 91 |
-
credentials: 'include'
|
| 92 |
-
})
|
| 93 |
-
.then(response => {
|
| 94 |
-
if (response.ok) {
|
| 95 |
-
// User is authenticated, redirect to home if not already there
|
| 96 |
-
if (window.location.pathname === '/login') {
|
| 97 |
-
window.location.href = '/';
|
| 98 |
-
}
|
| 99 |
-
} else {
|
| 100 |
-
// Not authenticated, redirect to login if not already there
|
| 101 |
-
if (window.location.pathname !== '/login') {
|
| 102 |
-
window.location.href = '/login';
|
| 103 |
-
}
|
| 104 |
-
}
|
| 105 |
-
})
|
| 106 |
-
.catch((error) => {
|
| 107 |
-
console.error('User status check error:', error);
|
| 108 |
-
if (window.location.pathname !== '/login') {
|
| 109 |
-
window.location.href = '/login';
|
| 110 |
-
}
|
| 111 |
-
});
|
| 112 |
const loginForm = document.getElementById('login-form');
|
| 113 |
const registerForm = document.getElementById('register-form');
|
| 114 |
-
const loginBtn = document.getElementById('login-btn');
|
| 115 |
|
| 116 |
// Login handler
|
| 117 |
-
loginForm.addEventListener('submit',
|
| 118 |
e.preventDefault();
|
| 119 |
|
| 120 |
-
const username = document.getElementById('username').value
|
| 121 |
const password = document.getElementById('password').value;
|
| 122 |
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
body: JSON.stringify({
|
| 141 |
-
username: username,
|
| 142 |
-
password: password
|
| 143 |
-
})
|
| 144 |
-
});
|
| 145 |
-
|
| 146 |
-
const data = await response.json();
|
| 147 |
-
console.log('Login response:', data); // Debug log
|
| 148 |
-
|
| 149 |
-
if (response.ok && data.success) {
|
| 150 |
-
// Store user info for UI only
|
| 151 |
-
if (data.user_id) {
|
| 152 |
-
localStorage.setItem('userId', data.user_id);
|
| 153 |
-
}
|
| 154 |
-
if (data.username) {
|
| 155 |
-
localStorage.setItem('username', data.username);
|
| 156 |
-
}
|
| 157 |
|
| 158 |
// Show success message
|
| 159 |
showAlert('success', 'Login successful! Redirecting...');
|
| 160 |
|
| 161 |
-
//
|
| 162 |
-
const redirectUrl = data.redirect_url || '/';
|
| 163 |
-
console.log('Redirecting to:', redirectUrl); // Debug log
|
| 164 |
-
|
| 165 |
-
// Try immediate redirect first
|
| 166 |
setTimeout(() => {
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
} catch (error) {
|
| 170 |
-
console.error('Redirect error:', error);
|
| 171 |
-
// Fallback redirect methods
|
| 172 |
-
try {
|
| 173 |
-
window.location.replace(redirectUrl);
|
| 174 |
-
} catch (error2) {
|
| 175 |
-
console.error('Replace redirect error:', error2);
|
| 176 |
-
// Last resort
|
| 177 |
-
window.location = redirectUrl;
|
| 178 |
-
}
|
| 179 |
-
}
|
| 180 |
-
}, 1000); // Reduced delay
|
| 181 |
-
|
| 182 |
} else {
|
| 183 |
-
|
| 184 |
-
showAlert('danger', data.detail || data.error || data.message || 'Login failed. Please try again.');
|
| 185 |
}
|
| 186 |
-
}
|
| 187 |
-
|
| 188 |
-
showAlert('danger', 'Network error
|
| 189 |
-
}
|
| 190 |
-
// Reset button state
|
| 191 |
-
loginBtn.disabled = false;
|
| 192 |
-
loginBtn.innerHTML = originalBtnText;
|
| 193 |
-
}
|
| 194 |
});
|
| 195 |
|
| 196 |
// Register handler
|
| 197 |
-
registerForm.addEventListener('submit',
|
| 198 |
e.preventDefault();
|
| 199 |
|
| 200 |
-
const username = document.getElementById('reg-username').value
|
| 201 |
-
const email = document.getElementById('reg-email').value
|
| 202 |
const password = document.getElementById('reg-password').value;
|
| 203 |
const confirmPassword = document.getElementById('reg-confirm-password').value;
|
| 204 |
|
| 205 |
-
if (!username || !email || !password || !confirmPassword) {
|
| 206 |
-
showAlert('danger', 'Please fill in all fields');
|
| 207 |
-
return;
|
| 208 |
-
}
|
| 209 |
-
|
| 210 |
if (password !== confirmPassword) {
|
| 211 |
showAlert('danger', 'Passwords do not match');
|
| 212 |
return;
|
| 213 |
}
|
| 214 |
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
password: password
|
| 230 |
-
})
|
| 231 |
-
});
|
| 232 |
-
|
| 233 |
-
const data = await response.json();
|
| 234 |
-
|
| 235 |
-
if (response.ok && data.success) {
|
| 236 |
const modal = bootstrap.Modal.getInstance(document.getElementById('registerModal'));
|
| 237 |
-
|
| 238 |
-
modal.hide();
|
| 239 |
-
}
|
| 240 |
showAlert('success', 'Account created successfully! Please log in.');
|
| 241 |
registerForm.reset();
|
| 242 |
} else {
|
| 243 |
-
showAlert('danger', data.
|
| 244 |
}
|
| 245 |
-
}
|
| 246 |
-
|
| 247 |
-
showAlert('danger', 'Network error
|
| 248 |
-
}
|
| 249 |
});
|
| 250 |
|
| 251 |
function showAlert(type, message) {
|
| 252 |
-
const alertContainer = document.getElementById('alert-container');
|
| 253 |
-
|
| 254 |
-
// Clear existing alerts
|
| 255 |
-
alertContainer.innerHTML = '';
|
| 256 |
-
|
| 257 |
const alert = document.createElement('div');
|
| 258 |
alert.className = `alert alert-${type} alert-dismissible fade show`;
|
| 259 |
alert.innerHTML = `
|
| 260 |
-
<i class="fas fa-${type === 'success' ? 'check
|
| 261 |
${message}
|
| 262 |
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
| 263 |
`;
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
setTimeout(() => {
|
| 270 |
-
if (alert.parentNode) {
|
| 271 |
-
alert.remove();
|
| 272 |
-
}
|
| 273 |
-
}, 5000);
|
| 274 |
-
}
|
| 275 |
-
}
|
| 276 |
-
|
| 277 |
-
// Check if user is already logged in (cookie-based)
|
| 278 |
-
fetch('/api/user/status', {
|
| 279 |
-
credentials: 'include'
|
| 280 |
-
})
|
| 281 |
-
.then(response => {
|
| 282 |
-
if (response.ok) {
|
| 283 |
-
// User is already logged in, redirect
|
| 284 |
-
console.log('User already logged in, redirecting...');
|
| 285 |
-
window.location.href = '/';
|
| 286 |
} else {
|
| 287 |
-
//
|
| 288 |
-
|
| 289 |
-
|
| 290 |
}
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
|
|
|
| 297 |
});
|
| 298 |
</script>
|
| 299 |
-
{% endblock %}
|
|
|
|
| 12 |
<h3 class="text-primary-custom">Welcome to ResearchMate</h3>
|
| 13 |
<p class="text-muted">Please log in to access your research projects</p>
|
| 14 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
<form id="login-form">
|
| 16 |
<div class="mb-3">
|
| 17 |
<label for="username" class="form-label">Username</label>
|
|
|
|
| 22 |
<input type="password" class="form-control" id="password" name="password" required>
|
| 23 |
</div>
|
| 24 |
<div class="d-grid">
|
| 25 |
+
<button type="submit" class="btn btn-primary">
|
| 26 |
<i class="fas fa-sign-in-alt me-2"></i>Login
|
| 27 |
</button>
|
| 28 |
</div>
|
|
|
|
| 52 |
<form id="register-form">
|
| 53 |
<div class="mb-3">
|
| 54 |
<label for="reg-username" class="form-label">Username</label>
|
| 55 |
+
<input type="text" class="form-control bg-white text-dark" id="reg-username" name="username" required>
|
| 56 |
</div>
|
| 57 |
<div class="mb-3">
|
| 58 |
<label for="reg-email" class="form-label">Email</label>
|
| 59 |
+
<input type="email" class="form-control bg-white text-dark" id="reg-email" name="email" required>
|
| 60 |
</div>
|
| 61 |
<div class="mb-3">
|
| 62 |
<label for="reg-password" class="form-label">Password</label>
|
| 63 |
+
<input type="password" class="form-control bg-white text-dark" id="reg-password" name="password" required>
|
| 64 |
</div>
|
| 65 |
<div class="mb-3">
|
| 66 |
<label for="reg-confirm-password" class="form-label">Confirm Password</label>
|
| 67 |
+
<input type="password" class="form-control bg-white text-dark" id="reg-confirm-password" name="confirm_password" required>
|
| 68 |
</div>
|
| 69 |
</form>
|
| 70 |
</div>
|
|
|
|
| 81 |
|
| 82 |
{% block extra_js %}
|
| 83 |
<script>
|
| 84 |
+
// Include authentication utilities
|
| 85 |
+
function setAuthToken(token) {
|
| 86 |
+
// Store in sessionStorage for better security (clears on browser close)
|
| 87 |
+
sessionStorage.setItem('authToken', token);
|
| 88 |
+
// Also store in localStorage for compatibility, but with shorter expiry
|
| 89 |
+
localStorage.setItem('authToken', token);
|
| 90 |
+
localStorage.setItem('tokenTimestamp', Date.now().toString());
|
| 91 |
+
|
| 92 |
+
// Set cookie with HttpOnly equivalent behavior
|
| 93 |
+
document.cookie = `authToken=${token}; path=/; SameSite=Strict; Secure=${location.protocol === 'https:'}`;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
const loginForm = document.getElementById('login-form');
|
| 98 |
const registerForm = document.getElementById('register-form');
|
|
|
|
| 99 |
|
| 100 |
// Login handler
|
| 101 |
+
loginForm.addEventListener('submit', function(e) {
|
| 102 |
e.preventDefault();
|
| 103 |
|
| 104 |
+
const username = document.getElementById('username').value;
|
| 105 |
const password = document.getElementById('password').value;
|
| 106 |
|
| 107 |
+
fetch('/api/auth/login', {
|
| 108 |
+
method: 'POST',
|
| 109 |
+
headers: {
|
| 110 |
+
'Content-Type': 'application/json',
|
| 111 |
+
},
|
| 112 |
+
body: JSON.stringify({
|
| 113 |
+
username: username,
|
| 114 |
+
password: password
|
| 115 |
+
})
|
| 116 |
+
})
|
| 117 |
+
.then(response => response.json())
|
| 118 |
+
.then(data => {
|
| 119 |
+
if (data.success) {
|
| 120 |
+
// Use secure token storage
|
| 121 |
+
setAuthToken(data.token);
|
| 122 |
+
sessionStorage.setItem('userId', data.user_id);
|
| 123 |
+
localStorage.setItem('userId', data.user_id);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
|
| 125 |
// Show success message
|
| 126 |
showAlert('success', 'Login successful! Redirecting...');
|
| 127 |
|
| 128 |
+
// Redirect to home page after a short delay
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
setTimeout(() => {
|
| 130 |
+
window.location.href = '/';
|
| 131 |
+
}, 1000);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
} else {
|
| 133 |
+
showAlert('danger', data.error || 'Login failed');
|
|
|
|
| 134 |
}
|
| 135 |
+
})
|
| 136 |
+
.catch(error => {
|
| 137 |
+
showAlert('danger', 'Network error: ' + error.message);
|
| 138 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
});
|
| 140 |
|
| 141 |
// Register handler
|
| 142 |
+
registerForm.addEventListener('submit', function(e) {
|
| 143 |
e.preventDefault();
|
| 144 |
|
| 145 |
+
const username = document.getElementById('reg-username').value;
|
| 146 |
+
const email = document.getElementById('reg-email').value;
|
| 147 |
const password = document.getElementById('reg-password').value;
|
| 148 |
const confirmPassword = document.getElementById('reg-confirm-password').value;
|
| 149 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
if (password !== confirmPassword) {
|
| 151 |
showAlert('danger', 'Passwords do not match');
|
| 152 |
return;
|
| 153 |
}
|
| 154 |
|
| 155 |
+
fetch('/api/auth/register', {
|
| 156 |
+
method: 'POST',
|
| 157 |
+
headers: {
|
| 158 |
+
'Content-Type': 'application/json',
|
| 159 |
+
},
|
| 160 |
+
body: JSON.stringify({
|
| 161 |
+
username: username,
|
| 162 |
+
email: email,
|
| 163 |
+
password: password
|
| 164 |
+
})
|
| 165 |
+
})
|
| 166 |
+
.then(response => response.json())
|
| 167 |
+
.then(data => {
|
| 168 |
+
if (data.success) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
const modal = bootstrap.Modal.getInstance(document.getElementById('registerModal'));
|
| 170 |
+
modal.hide();
|
|
|
|
|
|
|
| 171 |
showAlert('success', 'Account created successfully! Please log in.');
|
| 172 |
registerForm.reset();
|
| 173 |
} else {
|
| 174 |
+
showAlert('danger', data.error || 'Registration failed');
|
| 175 |
}
|
| 176 |
+
})
|
| 177 |
+
.catch(error => {
|
| 178 |
+
showAlert('danger', 'Network error: ' + error.message);
|
| 179 |
+
});
|
| 180 |
});
|
| 181 |
|
| 182 |
function showAlert(type, message) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
const alert = document.createElement('div');
|
| 184 |
alert.className = `alert alert-${type} alert-dismissible fade show`;
|
| 185 |
alert.innerHTML = `
|
| 186 |
+
<i class="fas fa-${type === 'success' ? 'check' : 'exclamation-triangle'} me-2"></i>
|
| 187 |
${message}
|
| 188 |
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
| 189 |
`;
|
| 190 |
+
|
| 191 |
+
// Try to insert into .container, else fallback to main content
|
| 192 |
+
const container = document.querySelector('.container');
|
| 193 |
+
if (container) {
|
| 194 |
+
container.insertBefore(alert, container.firstChild);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
} else {
|
| 196 |
+
// Fallback: insert at top of main content
|
| 197 |
+
const main = document.querySelector('main') || document.body;
|
| 198 |
+
main.insertBefore(alert, main.firstChild);
|
| 199 |
}
|
| 200 |
+
|
| 201 |
+
setTimeout(() => {
|
| 202 |
+
if (alert.parentNode) {
|
| 203 |
+
alert.remove();
|
| 204 |
+
}
|
| 205 |
+
}, 5000);
|
| 206 |
+
}
|
| 207 |
});
|
| 208 |
</script>
|
| 209 |
+
{% endblock %}
|