From 309ef8b2bab251a3580e8f2c9947c1ef89a957a9 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Wed, 2 Jul 2025 17:28:47 +0200 Subject: [PATCH 1/6] Save last login method in a cookie Initial attempt to save the "last login method" into a cookie so we can show a visual label next to the button to login. This is only the backend part. We need to write some code in the template to use this value next. I'm not happy with the implementation because looks pretty complex. I didn't find an easier way to do it and I'm open for suggestions. Required by https://github.com/readthedocs/ext-theme/issues/421 --- readthedocs/core/middleware.py | 23 +++++++++++++++++++++++ readthedocs/oauth/signals.py | 25 +++++++++++++++++++++++++ readthedocs/settings/base.py | 1 + 3 files changed, 49 insertions(+) diff --git a/readthedocs/core/middleware.py b/readthedocs/core/middleware.py index 2f0ed9c8cfd..8a9a0bd2fcc 100644 --- a/readthedocs/core/middleware.py +++ b/readthedocs/core/middleware.py @@ -77,3 +77,26 @@ def __call__(self, request): response._csp_update = update_csp_headers[url_name] return response + + +class LoginMethodCookie: + """ + Set a cookie with the login method used by the user. + + This is used by the templates to put a small "Last used" label next to the method used. + """ + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + response = self.get_response(request) + if request.user.is_authenticated and hasattr(request, "_last_login_method_used"): + response.set_cookie("last_login_method", request._last_login_method_used) + return response + + def process_template_response(self, request, response): + response.context_data = response.context_data or {} + response.context_data["last_login_method"] = request.COOKIES.get("last_login_method") + # response.context_data["last_login_method"] = "github" + return response diff --git a/readthedocs/oauth/signals.py b/readthedocs/oauth/signals.py index 0b244de064b..54d346a7a20 100644 --- a/readthedocs/oauth/signals.py +++ b/readthedocs/oauth/signals.py @@ -13,11 +13,36 @@ from readthedocs.oauth.notifications import MESSAGE_PROJECTS_TO_MIGRATE_TO_GITHUB_APP from readthedocs.oauth.tasks import sync_remote_repositories from readthedocs.projects.models import Feature +from django.utils import timezone log = structlog.get_logger(__name__) +@receiver(user_logged_in, sender=User) +def save_last_login_method_used(sender, request, user, *args, **kwargs): + """Save the login method used by the user. + + We don't know exactly what was the method used (regular user or social + account), so we check if any of the social account was used in the last 5 + seconds, if so, we save it as last method used. + + If we don't find a social account used in the last seconds, we set "email" + as login method. + + The method is saved in `Request._last_login_method_used`. This attribute is + read in the middleware to set the cookie in the response. + + Then, next time the user logs in, the middleware reads the cookie and + updates the context data. + """ + socialaccount = user.socialaccount_set.filter(last_login__gt=timezone.now() - timezone.timedelta(seconds=5)).first() + if socialaccount: + request._last_login_method_used = socialaccount.provider + else: + request._last_login_method_used = "email" + + @receiver(user_logged_in, sender=User) def sync_remote_repositories_on_login(sender, request, user, *args, **kwargs): """ diff --git a/readthedocs/settings/base.py b/readthedocs/settings/base.py index d5e2f8e2180..10fa5e804a3 100644 --- a/readthedocs/settings/base.py +++ b/readthedocs/settings/base.py @@ -338,6 +338,7 @@ def MIDDLEWARE(self): "readthedocs.core.middleware.UpdateCSPMiddleware", "simple_history.middleware.HistoryRequestMiddleware", "readthedocs.core.logs.ReadTheDocsRequestMiddleware", + "readthedocs.core.middleware.LoginMethodCookie", "django_structlog.middlewares.CeleryMiddleware", ] if self.SHOW_DEBUG_TOOLBAR: From efae7e07b14a00a8fd839b776221c19a9749476d Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Mon, 14 Jul 2025 14:21:16 +0200 Subject: [PATCH 2/6] Receive the last login method from a cookie in the front-end --- readthedocs/core/middleware.py | 16 ++++++++++++---- readthedocs/oauth/signals.py | 25 ------------------------- 2 files changed, 12 insertions(+), 29 deletions(-) diff --git a/readthedocs/core/middleware.py b/readthedocs/core/middleware.py index 8a9a0bd2fcc..64201a8884d 100644 --- a/readthedocs/core/middleware.py +++ b/readthedocs/core/middleware.py @@ -91,12 +91,20 @@ def __init__(self, get_response): def __call__(self, request): response = self.get_response(request) - if request.user.is_authenticated and hasattr(request, "_last_login_method_used"): - response.set_cookie("last_login_method", request._last_login_method_used) return response def process_template_response(self, request, response): response.context_data = response.context_data or {} - response.context_data["last_login_method"] = request.COOKIES.get("last_login_method") - # response.context_data["last_login_method"] = "github" + last_login_method = request.COOKIES.get("last-login-method") + response.context_data["last_login_method"] = last_login_method + + login_tab = None + if last_login_method == "email": + login_tab = "email" + if last_login_method in ("githubapp", "github", "gitlab", "bitbucket"): + login_tab = "vcs" + if last_login_method in "sso": + login_tab = "sso" + log.debug("Login method.", last_login_method=last_login_method, login_tab=login_tab) + response.context_data["login_tab"] = login_tab return response diff --git a/readthedocs/oauth/signals.py b/readthedocs/oauth/signals.py index 54d346a7a20..0b244de064b 100644 --- a/readthedocs/oauth/signals.py +++ b/readthedocs/oauth/signals.py @@ -13,36 +13,11 @@ from readthedocs.oauth.notifications import MESSAGE_PROJECTS_TO_MIGRATE_TO_GITHUB_APP from readthedocs.oauth.tasks import sync_remote_repositories from readthedocs.projects.models import Feature -from django.utils import timezone log = structlog.get_logger(__name__) -@receiver(user_logged_in, sender=User) -def save_last_login_method_used(sender, request, user, *args, **kwargs): - """Save the login method used by the user. - - We don't know exactly what was the method used (regular user or social - account), so we check if any of the social account was used in the last 5 - seconds, if so, we save it as last method used. - - If we don't find a social account used in the last seconds, we set "email" - as login method. - - The method is saved in `Request._last_login_method_used`. This attribute is - read in the middleware to set the cookie in the response. - - Then, next time the user logs in, the middleware reads the cookie and - updates the context data. - """ - socialaccount = user.socialaccount_set.filter(last_login__gt=timezone.now() - timezone.timedelta(seconds=5)).first() - if socialaccount: - request._last_login_method_used = socialaccount.provider - else: - request._last_login_method_used = "email" - - @receiver(user_logged_in, sender=User) def sync_remote_repositories_on_login(sender, request, user, *args, **kwargs): """ From a96d7a7acdf28dae609f27019eb789e09479183c Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Mon, 14 Jul 2025 14:44:42 +0200 Subject: [PATCH 3/6] Rename variable --- readthedocs/core/middleware.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/readthedocs/core/middleware.py b/readthedocs/core/middleware.py index 64201a8884d..66fb35c6271 100644 --- a/readthedocs/core/middleware.py +++ b/readthedocs/core/middleware.py @@ -98,13 +98,17 @@ def process_template_response(self, request, response): last_login_method = request.COOKIES.get("last-login-method") response.context_data["last_login_method"] = last_login_method - login_tab = None + last_login_tab = None if last_login_method == "email": - login_tab = "email" + last_login_tab = "email" if last_login_method in ("githubapp", "github", "gitlab", "bitbucket"): - login_tab = "vcs" + last_login_tab = "vcs" if last_login_method in "sso": - login_tab = "sso" - log.debug("Login method.", last_login_method=last_login_method, login_tab=login_tab) - response.context_data["login_tab"] = login_tab + last_login_tab = "sso" + log.debug( + "Login method.", + last_login_method=last_login_method, + last_login_tab=last_login_tab, + ) + response.context_data["last_login_tab"] = last_login_tab return response From dad68564af7a69a5b0ce2c82e414e805dcab8dd1 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Mon, 14 Jul 2025 16:47:44 +0200 Subject: [PATCH 4/6] Conditional --- readthedocs/core/middleware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readthedocs/core/middleware.py b/readthedocs/core/middleware.py index 66fb35c6271..c3fd4e37f25 100644 --- a/readthedocs/core/middleware.py +++ b/readthedocs/core/middleware.py @@ -103,7 +103,7 @@ def process_template_response(self, request, response): last_login_tab = "email" if last_login_method in ("githubapp", "github", "gitlab", "bitbucket"): last_login_tab = "vcs" - if last_login_method in "sso": + if last_login_method == "sso": last_login_tab = "sso" log.debug( "Login method.", From c96c5db0a6971eb3ce91cf5237d8defecfebd9ab Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Mon, 14 Jul 2025 17:21:04 +0200 Subject: [PATCH 5/6] Update providers --- readthedocs/core/middleware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readthedocs/core/middleware.py b/readthedocs/core/middleware.py index c3fd4e37f25..ca70852f2e8 100644 --- a/readthedocs/core/middleware.py +++ b/readthedocs/core/middleware.py @@ -101,7 +101,7 @@ def process_template_response(self, request, response): last_login_tab = None if last_login_method == "email": last_login_tab = "email" - if last_login_method in ("githubapp", "github", "gitlab", "bitbucket"): + if last_login_method in ("githubapp", "github", "gitlab", "bitbucket_oauth2", "google"): last_login_tab = "vcs" if last_login_method == "sso": last_login_tab = "sso" From 4ff82e18f443e17e40f17a81e26d8286c9d6c651 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Mon, 14 Jul 2025 18:22:17 +0200 Subject: [PATCH 6/6] Override the view instead of using a middleware --- readthedocs/core/middleware.py | 35 ---------------------------------- readthedocs/profiles/views.py | 24 ++++++++++++++++++++++- readthedocs/settings/base.py | 1 - 3 files changed, 23 insertions(+), 37 deletions(-) diff --git a/readthedocs/core/middleware.py b/readthedocs/core/middleware.py index ca70852f2e8..2f0ed9c8cfd 100644 --- a/readthedocs/core/middleware.py +++ b/readthedocs/core/middleware.py @@ -77,38 +77,3 @@ def __call__(self, request): response._csp_update = update_csp_headers[url_name] return response - - -class LoginMethodCookie: - """ - Set a cookie with the login method used by the user. - - This is used by the templates to put a small "Last used" label next to the method used. - """ - - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - response = self.get_response(request) - return response - - def process_template_response(self, request, response): - response.context_data = response.context_data or {} - last_login_method = request.COOKIES.get("last-login-method") - response.context_data["last_login_method"] = last_login_method - - last_login_tab = None - if last_login_method == "email": - last_login_tab = "email" - if last_login_method in ("githubapp", "github", "gitlab", "bitbucket_oauth2", "google"): - last_login_tab = "vcs" - if last_login_method == "sso": - last_login_tab = "sso" - log.debug( - "Login method.", - last_login_method=last_login_method, - last_login_tab=last_login_tab, - ) - response.context_data["last_login_tab"] = last_login_tab - return response diff --git a/readthedocs/profiles/views.py b/readthedocs/profiles/views.py index 9aa51e5da06..9024865ed8e 100644 --- a/readthedocs/profiles/views.py +++ b/readthedocs/profiles/views.py @@ -3,6 +3,7 @@ from enum import StrEnum from enum import auto +import structlog from allauth.account.views import LoginView as AllAuthLoginView from allauth.account.views import LogoutView as AllAuthLogoutView from allauth.socialaccount.providers.github.provider import GitHubProvider @@ -52,8 +53,29 @@ from readthedocs.projects.utils import get_csv_file +log = structlog.get_logger(__name__) + + class LoginViewBase(AllAuthLoginView): - pass + def get_context_data(self, **kwargs): + context_data = super().get_context_data(**kwargs) + last_login_method = self.request.COOKIES.get("last-login-method") + context_data["last_login_method"] = last_login_method + + last_login_tab = None + if last_login_method == "email": + last_login_tab = "email" + if last_login_method in ("githubapp", "github", "gitlab", "bitbucket_oauth2", "google"): + last_login_tab = "vcs" + if last_login_method == "sso": + last_login_tab = "sso" + log.debug( + "Login method.", + last_login_method=last_login_method, + last_login_tab=last_login_tab, + ) + context_data["last_login_tab"] = last_login_tab + return context_data class LoginView(SettingsOverrideObject): diff --git a/readthedocs/settings/base.py b/readthedocs/settings/base.py index 3b00c35104f..126d61507f8 100644 --- a/readthedocs/settings/base.py +++ b/readthedocs/settings/base.py @@ -339,7 +339,6 @@ def MIDDLEWARE(self): "readthedocs.core.middleware.UpdateCSPMiddleware", "simple_history.middleware.HistoryRequestMiddleware", "readthedocs.core.logs.ReadTheDocsRequestMiddleware", - "readthedocs.core.middleware.LoginMethodCookie", "django_structlog.middlewares.RequestMiddleware", ] if self.SHOW_DEBUG_TOOLBAR: