-
Notifications
You must be signed in to change notification settings - Fork 0
Cookie ids #146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Cookie ids #146
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |||||
| from starlette.middleware.base import BaseHTTPMiddleware | ||||||
|
|
||||||
| from src.app.services.sql_db.queries import register_endpoint | ||||||
| from src.app.shared.utils.requests import extract_session_cookie | ||||||
| from src.app.utils.logger import logger as logger_utils | ||||||
|
|
||||||
| logger = logger_utils(__name__) | ||||||
|
|
@@ -11,7 +12,7 @@ | |||||
| class MonitorRequestsMiddleware(BaseHTTPMiddleware): | ||||||
| async def dispatch(self, request: Request, call_next): | ||||||
| if request.url.path.startswith("/api/v1/"): | ||||||
| session_id = request.headers.get("X-Session-ID") | ||||||
| session_id = extract_session_cookie(request) | ||||||
| if session_id: | ||||||
| try: | ||||||
| await run_in_threadpool( | ||||||
|
|
@@ -24,7 +25,7 @@ async def dispatch(self, request: Request, call_next): | |||||
| logger.error(f"Failed to register endpoint {request.url.path}: {e}") | ||||||
| else: | ||||||
| logger.warning( | ||||||
| f"No X-Session-ID header provided for {request.url.path}" | ||||||
| f"No X-Session-ID cookie provided for {request.url.path}" | ||||||
|
||||||
| f"No X-Session-ID cookie provided for {request.url.path}" | |
| f"No x-session-id cookie provided for {request.url.path}" |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |||||||||
| import uuid | ||||||||||
| from datetime import datetime, timedelta | ||||||||||
|
|
||||||||||
| from fastapi import HTTPException | ||||||||||
| from sqlalchemy.sql import select | ||||||||||
| from welearn_database.data.models import Bookmark, InferredUser, Session | ||||||||||
|
|
||||||||||
|
|
@@ -27,6 +28,12 @@ def get_or_create_user_sync( | |||||||||
| ).first() | ||||||||||
| if user: | ||||||||||
| return user.id | ||||||||||
| else: | ||||||||||
| logger.error(f"User with id {user_id} not found,") | ||||||||||
|
||||||||||
| logger.error(f"User with id {user_id} not found,") | |
| logger.error(f"User with id {user_id} not found") |
Copilot
AI
Mar 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
get_user_from_session_id currently treats any session row as valid and does not check Session.end_at > now (unlike get_or_create_session_sync). This can make expired sessions resolve to a user indefinitely, especially problematic now that cookies can persist long-term. Consider filtering by end_at in the query (and/or deleting/invalidating expired sessions).
Copilot
AI
Mar 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
get_user_from_session_id instantiates an HTTPException in the else branch but never raises it, so the exception is effectively ignored and the function falls through returning None. Either raise the exception or remove it and just return None (and let callers decide how to handle missing sessions).
| else: | |
| HTTPException( | |
| status_code=404, detail=f"Session with id {session_id} not found" | |
| ) |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -25,3 +25,7 @@ | |||||
| } | ||||||
|
|
||||||
| APP_NAME = "welearn-api" | ||||||
|
|
||||||
|
|
||||||
| SESSION_COOKIE_NAME = "x-session-id" | ||||||
| SESSION_TTL_SECONDS = 60 * 60 * 24 * 400 # 400 days | ||||||
|
||||||
| SESSION_TTL_SECONDS = 60 * 60 * 24 * 400 # 400 days | |
| SESSION_TTL_SECONDS = 60 * 60 * 24 # 1 day |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| from typing import Optional | ||
| from uuid import UUID | ||
|
|
||
| from fastapi import Request | ||
|
|
||
| from src.app.shared.domain.constants import SESSION_COOKIE_NAME | ||
| from src.app.utils.logger import logger as utils_logger | ||
|
|
||
| logger = utils_logger(__name__) | ||
|
|
||
|
|
||
| def extract_session_cookie(request: Request) -> Optional[UUID]: | ||
| cookie_value = request.cookies.get(SESSION_COOKIE_NAME) | ||
| if not cookie_value: | ||
| return None | ||
|
|
||
| try: | ||
| return UUID(cookie_value) | ||
| except ValueError: | ||
| logger.warning("Invalid session cookie format: %s", cookie_value) | ||
| return None | ||
|
|
||
|
|
||
| def extract_origin_from_request(request: Request) -> str: | ||
| return request.headers.get("origin", "unknown") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| import unittest | ||
| import uuid | ||
| from unittest import mock | ||
| from unittest.mock import MagicMock | ||
|
|
||
|
|
@@ -231,30 +232,36 @@ async def test_get_user_bookmarks_success_empty(self, session_maker_mock, *mocks | |
| session.execute.return_value.all.return_value = [] | ||
| session_maker_mock.return_value.__enter__.return_value = session | ||
|
|
||
| user_id = "22222222-2222-2222-2222-222222222222" | ||
| response = client.get( | ||
| f"{settings.API_V1_STR}/user/:user_id/bookmarks", | ||
| params={"user_id": user_id}, | ||
| f"{settings.API_V1_STR}/user/bookmarks", | ||
| headers={"X-API-Key": "test"}, | ||
| cookies={"x-session-id": "bdb62bb2-1fe5-4d14-92fd-60a041355aea"}, | ||
| ) | ||
| self.assertEqual(response.status_code, 200) | ||
| self.assertEqual(response.json(), {"bookmarks": []}) | ||
|
|
||
| @mock.patch("src.app.services.sql_db.queries_user.session_maker") | ||
| async def test_add_user_bookmark_success(self, session_maker_mock, *mocks): | ||
| """Ajout d'un bookmark""" | ||
| session = MagicMock() | ||
| session.execute.return_value.first.side_effect = [MagicMock(id="user-1"), None] | ||
| session_maker_mock.return_value.__enter__.return_value = session | ||
| @mock.patch("src.app.user.api.router.run_in_threadpool") | ||
| @mock.patch("src.app.user.api.router.resolve_user_and_session") | ||
| async def test_add_user_bookmark_success( | ||
| self, resolve_user_and_session_mock, run_in_threadpool_mock, *mocks | ||
| ): | ||
| """Ajout d'un bookmark - mocks only what is needed""" | ||
| # Mock resolve_user_and_session to return user_id and session_id | ||
| user_id = uuid.UUID("cfc8072c-a055-442a-9878-b5a73d9141b2") | ||
| session_id = uuid.UUID("bdb62bb2-1fe5-4d14-92fd-60a041355aea") | ||
| resolve_user_and_session_mock.return_value = (user_id, session_id) | ||
|
|
||
| user_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" | ||
| # Mock run_in_threadpool to simulate DB add_user_bookmark_sync | ||
| document_id = "ffffffff-ffff-ffff-ffff-ffffffffffff" | ||
| run_in_threadpool_mock.return_value = document_id | ||
|
|
||
| response = client.post( | ||
| f"{settings.API_V1_STR}/user/:user_id/bookmarks/:document_id", | ||
| params={"user_id": user_id, "document_id": document_id}, | ||
| f"{settings.API_V1_STR}/user/bookmarks/:document_id", | ||
| params={"document_id": document_id}, | ||
| headers={"X-API-Key": "test"}, | ||
| cookies={"x-session-id": str(session_id)}, | ||
| ) | ||
|
Comment on lines
258
to
263
|
||
| self.assertEqual(response.status_code, 200) | ||
| self.assertEqual(response.json(), {"added": document_id}) | ||
| session.add.assert_called_once() | ||
| session.commit.assert_called_once() | ||
| resolve_user_and_session_mock.assert_called_once() | ||
| run_in_threadpool_mock.assert_called_once() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ENVis now a requiredSettingsfield. Becausesettings = Settings()is instantiated at import time, missingENVwill prevent the app from starting (ValidationError). If you want backward compatibility / safer local runs, consider givingENVa default (e.g.,"development") and/or validating it as a constrained enum.