Ananthakr1shnan commited on
Commit
356ac4f
·
1 Parent(s): f20194e

Updated files

Browse files
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", "main.py"]
 
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.eyJ1c2VyX2lkIjoiYWRtaW5fdXNlciIsInVzZXJuYW1lIjoiYWRtaW4iLCJleHAiOjE3NTI1NDc2NjR9.SZ56Rw7mAUJgfScxu1ElGfI5dfWuUxTwUX6xi3d_7a8",
4
- "created_at": "2025-07-15T00:17:44.795086",
5
- "last_activity": "2025-07-15T00:50:22.820489"
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
- FastAPI compatible authentication module
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 pathlib import Path
 
 
 
 
 
 
 
 
 
 
 
 
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
- # Use absolute paths and handle Hugging Face Spaces
18
- self.base_dir = Path(__file__).parent.parent # Go up from src/components
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 with better error handling"""
43
- try:
44
- self.data_dir.mkdir(parents=True, exist_ok=True)
45
-
46
- if not self.users_file.exists():
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
- try:
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 or memory"""
86
- if self.use_memory:
87
- return self.users_memory.copy()
88
-
89
  try:
90
- if self.users_file.exists():
91
- with open(self.users_file, 'r') as f:
92
- users = json.load(f)
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 or memory"""
101
- if self.use_memory:
102
- self.users_memory = users.copy()
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 with detailed logging"""
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
- try:
160
- token = jwt.encode({
161
- 'user_id': user['user_id'],
162
- 'username': username,
163
- 'exp': datetime.utcnow() + timedelta(hours=8)
164
- }, self.secret_key, algorithm='HS256')
165
-
166
- # Track active session
167
- self.add_active_session(user['user_id'], token)
168
-
169
- print(f"✅ Authentication successful for '{username}'")
170
- return {
171
- 'success': True,
172
- 'token': token,
173
- 'user_id': user['user_id'],
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
- except Exception as e:
200
- print(f"❌ Token verification error: {e}")
 
 
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
- try:
216
- users[admin_username] = {
217
- 'user_id': admin_user_id,
218
- 'email': 'admin@researchmate.local',
219
- 'password_hash': self.hash_password('admin123'),
220
- 'created_at': datetime.now().isoformat(),
221
- 'is_active': True,
222
- 'is_admin': True
223
- }
224
-
225
- self.save_users(users)
226
- return {
227
- 'success': True,
228
- 'message': 'Default admin user created',
229
- 'username': admin_username,
230
- 'password': 'admin123'
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 self.session_file.exists():
243
  with open(self.session_file, 'r') as f:
244
  return json.load(f)
245
- except Exception as e:
246
- print(f"❌ Error loading sessions: {e}")
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 Exception as e:
259
- print(f"❌ Error saving sessions: {e}")
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
- try:
283
- created_at = datetime.fromisoformat(session['created_at'])
284
- if datetime.now() - created_at > timedelta(hours=8):
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
- try:
319
- created_at = datetime.fromisoformat(session['created_at'])
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 debug_status(self):
334
- """Debug authentication status"""
335
- users = self.load_users()
336
  sessions = self.load_active_sessions()
337
-
338
- print("=== AUTH DEBUG STATUS ===")
339
- print(f"Storage mode: {'Memory' if self.use_memory else 'File'}")
340
- print(f"Users file exists: {self.users_file.exists() if not self.use_memory else 'N/A'}")
341
- print(f"Total users: {len(users)}")
342
- print(f"Active sessions: {len(sessions)}")
343
-
344
- if users:
345
- print("Users:")
346
- for username, user_data in users.items():
347
- print(f" - {username}: {user_data.get('user_id', 'No ID')}")
348
-
349
- print("========================")
 
 
 
 
 
 
 
 
 
350
 
351
- # Global auth manager instance
352
- auth_manager = AuthManager()
 
 
 
 
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 main import app
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 main
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/session handled by backend HttpOnly cookie and /api/user/status
7
- // All API calls should use apiRequest with credentials: 'include'.
8
- // No token storage or Authorization header needed.
9
- // Session timeout and activity tracking handled server-side.
 
 
 
 
 
 
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
- // Document ready
13
- document.addEventListener('DOMContentLoaded', function() {
14
- // Always verify authentication with backend on page load
15
- fetch('/api/user/status', {
16
- credentials: 'include'
17
- })
18
- .then(response => {
19
- if (response.ok) {
20
- // User is authenticated, continue
21
- // Optionally, start session timeout or other logic here
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  } else {
23
- // Not authenticated, redirect to login if not already there
24
- if (window.location.pathname !== '/login') {
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
- // (trackActivity removed: no longer needed)
 
 
 
 
 
 
 
 
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(this.getAttribute('href'));
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
- async function initializeUpload() {
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
- await restoreUploadResults();
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
- async function saveUploadResults(data) {
219
  try {
220
- const currentUser = await getCurrentUserId();
221
- if (!currentUser) {
222
- console.warn('Cannot save upload results: no user');
 
 
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
- async function restoreUploadResults() {
238
  try {
239
  const resultsContainer = document.getElementById('results-container');
240
  if (!resultsContainer) return;
241
- // Get current user from backend
242
- const currentUser = await getCurrentUserId();
 
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
- // Check if data is recent (max 1 hour)
 
 
 
 
 
 
 
 
 
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 for user');
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 from backend
275
- async function getCurrentUserId() {
276
  try {
277
- const response = await fetch('/api/user/status', { credentials: 'include' });
278
- if (!response.ok) return null;
279
- const data = await response.json();
280
- // Try user_id, id, or username as fallback
281
- return data.user_id || data.id || data.username || null;
 
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
- // Clear user info from localStorage
1062
- localStorage.removeItem('userId');
1063
- localStorage.removeItem('username');
 
 
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" id="login-btn">
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
- <input type="text" class="form-control bg-white text-dark" id="reg-username" name="username" required>
60
  </div>
61
  <div class="mb-3">
62
  <label for="reg-email" class="form-label">Email</label>
63
- <input type="email" class="form-control bg-white text-dark" id="reg-email" name="email" required>
64
  </div>
65
  <div class="mb-3">
66
  <label for="reg-password" class="form-label">Password</label>
67
- <input type="password" class="form-control bg-white text-dark" id="reg-password" name="password" required>
68
  </div>
69
  <div class="mb-3">
70
  <label for="reg-confirm-password" class="form-label">Confirm Password</label>
71
- <input type="password" class="form-control bg-white text-dark" id="reg-confirm-password" name="confirm_password" required>
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', async function(e) {
118
  e.preventDefault();
119
 
120
- const username = document.getElementById('username').value.trim();
121
  const password = document.getElementById('password').value;
122
 
123
- if (!username || !password) {
124
- showAlert('danger', 'Please fill in all fields');
125
- return;
126
- }
127
-
128
- // Show loading state
129
- const originalBtnText = loginBtn.innerHTML;
130
- loginBtn.disabled = true;
131
- loginBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Logging in...';
132
-
133
- try {
134
- const response = await fetch('/api/auth/login', {
135
- method: 'POST',
136
- headers: {
137
- 'Content-Type': 'application/json',
138
- },
139
- credentials: 'include', // Important: Include cookies
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
- // Multiple redirect strategies
162
- const redirectUrl = data.redirect_url || '/';
163
- console.log('Redirecting to:', redirectUrl); // Debug log
164
-
165
- // Try immediate redirect first
166
  setTimeout(() => {
167
- try {
168
- window.location.href = redirectUrl;
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
- console.error('Login failed:', data); // Debug log
184
- showAlert('danger', data.detail || data.error || data.message || 'Login failed. Please try again.');
185
  }
186
- } catch (error) {
187
- console.error('Login error:', error);
188
- showAlert('danger', 'Network error. Please check your connection and try again.');
189
- } finally {
190
- // Reset button state
191
- loginBtn.disabled = false;
192
- loginBtn.innerHTML = originalBtnText;
193
- }
194
  });
195
 
196
  // Register handler
197
- registerForm.addEventListener('submit', async function(e) {
198
  e.preventDefault();
199
 
200
- const username = document.getElementById('reg-username').value.trim();
201
- const email = document.getElementById('reg-email').value.trim();
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
- if (password.length < 6) {
216
- showAlert('danger', 'Password must be at least 6 characters long');
217
- return;
218
- }
219
-
220
- try {
221
- const response = await fetch('/api/auth/register', {
222
- method: 'POST',
223
- headers: {
224
- 'Content-Type': 'application/json',
225
- },
226
- body: JSON.stringify({
227
- username: username,
228
- email: email,
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
- if (modal) {
238
- modal.hide();
239
- }
240
  showAlert('success', 'Account created successfully! Please log in.');
241
  registerForm.reset();
242
  } else {
243
- showAlert('danger', data.detail || data.error || data.message || 'Registration failed. Please try again.');
244
  }
245
- } catch (error) {
246
- console.error('Registration error:', error);
247
- showAlert('danger', 'Network error. Please check your connection and try again.');
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-circle' : 'exclamation-triangle'} me-2"></i>
261
  ${message}
262
  <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
263
  `;
264
-
265
- alertContainer.appendChild(alert);
266
-
267
- // Auto-remove after 5 seconds for error messages
268
- if (type === 'danger') {
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
- // Not logged in, clear any UI user info
288
- localStorage.removeItem('userId');
289
- localStorage.removeItem('username');
290
  }
291
- })
292
- .catch((error) => {
293
- console.error('User status check error:', error);
294
- localStorage.removeItem('userId');
295
- localStorage.removeItem('username');
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 %}