From cb6301e88c5cf61dcb28b4affae1ae095a730a8f Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Thu, 23 Apr 2026 15:36:18 -0400 Subject: [PATCH 01/11] Rework auth views/routes/redirects --- framework/auth/utils.py | 7 +++++++ framework/auth/views.py | 6 +++--- website/routes.py | 19 ++++-------------- website/views.py | 43 +++++++++++++++++++++++------------------ 4 files changed, 38 insertions(+), 37 deletions(-) diff --git a/framework/auth/utils.py b/framework/auth/utils.py index 520c9489e1b..8f5bbca97b9 100644 --- a/framework/auth/utils.py +++ b/framework/auth/utils.py @@ -11,6 +11,7 @@ from framework import sentry from website import settings +from website.util import web_url_for logger = logging.getLogger(__name__) @@ -169,3 +170,9 @@ def generate_csl_given_name(given_name, middle_names='', suffix=''): if suffix: given = f'{given}, {suffix}' return given + +def get_default_osf_logout_url(): + """Return the default OSF logout URL. + """ + next_url = web_url_for(view_name='index', _absolute=True) + return web_url_for(view_name='auth_logout', _absolute=True, next=next_url) diff --git a/framework/auth/views.py b/framework/auth/views.py index 59b2a207af4..02fef28ab24 100644 --- a/framework/auth/views.py +++ b/framework/auth/views.py @@ -84,7 +84,7 @@ def _reset_password_get(auth, uid=None, token=None, institutional=False): raise HTTPError(http_status.HTTP_400_BAD_REQUEST, data=error_data) # override routes.py login_url to redirect to my-projects - service_url = web_url_for('my_projects', _absolute=True) + service_url = web_url_for('dashboard', _absolute=True) return { 'uid': user_obj._id, @@ -176,7 +176,7 @@ def forgot_password_get(auth): #overriding the routes.py sign in url to redirect to the my-projects after login context = {} - context['login_url'] = web_url_for('my_projects', _absolute=True) + context['login_url'] = web_url_for('dashboard', _absolute=True) return context @@ -391,7 +391,7 @@ def login_and_register_handler(auth, login=True, campaign=None, next_url=None, l # `/login/` or `/register/` without any parameter if auth.logged_in: data['status_code'] = http_status.HTTP_302_FOUND - data['next_url'] = web_url_for('my_projects', _absolute=True) + data['next_url'] = web_url_for('index', _absolute=True) return data diff --git a/website/routes.py b/website/routes.py index 226f03fb1f1..ec7245fd00d 100644 --- a/website/routes.py +++ b/website/routes.py @@ -230,11 +230,10 @@ def sitemap_file(path): def goodbye(): # Redirect to dashboard if logged in - redirect_url = util.web_url_for('auth_login') if _get_current_user(): - return redirect(redirect_url) + return redirect(util.web_url_for('dashboard')) else: - return redirect(redirect_url + '?goodbye=true') + return redirect(util.web_url_for('index')) def make_url_map(app): """Set up all the routes for the OSF app. @@ -294,12 +293,8 @@ def make_url_map(app): process_rules(app, [ Rule('/', 'get', website_views.index, notemplate), - Rule( - '/dashboard/', - 'get', - website_views.dashboard, - notemplate - ), + Rule('/dashboard/', 'get', website_views.dashboard, notemplate), + Rule('/my-projects/', 'get', website_views.my_projects, notemplate), Rule( '/metadata//', @@ -307,12 +302,6 @@ def make_url_map(app): website_views.metadata_download, notemplate ), - Rule( - '/my-projects/', - 'get', - website_views.my_projects, - OsfWebRenderer('my_projects.mako', trust=False) - ), Rule( '/reproducibility/', diff --git a/website/views.py b/website/views.py index 1a4bf4942da..f9601fb0441 100644 --- a/website/views.py +++ b/website/views.py @@ -8,8 +8,10 @@ from django.apps import apps from flask import request, Response +from framework import sentry from framework.auth import Auth -from framework.auth.decorators import must_be_logged_in, is_contributor_or_public_resource +from framework.auth.utils import get_default_osf_logout_url +from framework.auth.decorators import is_contributor_or_public_resource from framework.auth.forms import SignInForm, ForgotPasswordForm from framework.exceptions import HTTPError from framework.flask import redirect # VOL-aware redirect @@ -28,7 +30,6 @@ from osf.utils import permissions from osf.metadata.tools import pls_gather_metadata_file -from api.waffle.utils import storage_i18n_flag_active logger = logging.getLogger(__name__) @@ -132,21 +133,6 @@ def find_bookmark_collection(user): return Collection.objects.get(creator=user, deleted__isnull=True, is_bookmark_collection=True) -@must_be_logged_in -def my_projects(auth): - user = auth.user - - region_list = get_storage_region_list(user) - - bookmark_collection = find_bookmark_collection(user) - my_projects_id = bookmark_collection._id - return {'addons_enabled': user.get_addon_names(), - 'dashboard_id': my_projects_id, - 'storage_regions': region_list, - 'storage_flag_is_active': storage_i18n_flag_active(), - } - - def validate_page_num(page, pages): if page < 0 or (pages and page >= pages): raise HTTPError(http_status.HTTP_400_BAD_REQUEST, data=dict( @@ -165,11 +151,30 @@ def paginate(items, total, page, size): def index(): - return redirect('/my-projects/') + """This route is handled by Angular now and web flow should not reach it at all. + There is alo no direct call of this view other than via `website/routes`. However, + we kept this view to use `web_url_for()` to build correct URL to go to Angular. + """ + sentry.log_message('View "index" should not have been directly called or reached') + return redirect(get_default_osf_logout_url()) def dashboard(): - return redirect('/my-projects/') + """ This route is handled by Angular now and web flow should not reach it at all. + There is alo no direct call of this view other than via `website/routes`. However, + we kept this view to use `web_url_for()` to build correct URL to go to Angular. + """ + sentry.log_message('View "dashboard" should not have been directly called or reached') + return redirect(get_default_osf_logout_url()) + + +def my_projects(): + """ This route is handled by Angular now and web flow should not reach it at all. + There is alo no direct call of this view other than via `website/routes`. However, + we kept this view to use `web_url_for()` to build correct URL to go to Angular. + """ + sentry.log_message('View "my_projects" should not have been directly called or reached') + return redirect(get_default_osf_logout_url()) def reproducibility(): From 208fbf5e867b0fc05c13e867fef57c9c7f55b24b Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Thu, 23 Apr 2026 15:40:55 -0400 Subject: [PATCH 02/11] Fix incorrect handling of invalid session/cookie --- framework/sessions/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/framework/sessions/__init__.py b/framework/sessions/__init__.py index 5891cb6d69f..bc11b690b12 100644 --- a/framework/sessions/__init__.py +++ b/framework/sessions/__init__.py @@ -15,7 +15,6 @@ from osf.utils.fields import ensure_str from osf.exceptions import InvalidCookieOrSessionError from website import settings -from website.util import web_url_for SessionStore = import_module(django_conf_settings.SESSION_ENGINE).SessionStore @@ -165,7 +164,7 @@ def create_session(response, data=None): def before_request(): # TODO: Fix circular import from framework.auth.core import get_user - from framework.auth import cas + from framework.auth import cas, utils UserSessionMap = apps.get_model('osf.UserSessionMap') # Request Type 1: Service ticket validation during CAS login. @@ -216,7 +215,8 @@ def before_request(): try: user_session = flask_get_session_from_cookie(cookie) except InvalidCookieOrSessionError: - response = redirect(web_url_for('auth_login')) + # If invalid session/cookie happens, perform a full logout to clear both CAS and OSF Sessions + response = redirect(utils.get_default_osf_logout_url()) response.delete_cookie(settings.COOKIE_NAME, domain=settings.OSF_COOKIE_DOMAIN) return response # Case 1: anonymous session that is used for first time external (e.g. ORCiD) login only From 6c8ca7aa9cd77d83a41dbcef6b75900bfbf52218 Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Fri, 24 Apr 2026 14:30:30 -0400 Subject: [PATCH 03/11] Add local mode and update web_url_for to support angular domain --- website/settings/defaults.py | 3 ++- website/settings/local-ci.py | 2 ++ website/settings/local-dist.py | 3 ++- website/util/__init__.py | 7 ++++++- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/website/settings/defaults.py b/website/settings/defaults.py index 75edc0bd5a6..c2ad5bff701 100644 --- a/website/settings/defaults.py +++ b/website/settings/defaults.py @@ -84,13 +84,14 @@ def parent_dir(path): DEV_MODE = False DEBUG_MODE = False SECURE_MODE = not DEBUG_MODE # Set secure cookie +LOCAL_MODE = False # handles angular and web with different domains in local env PROTOCOL = 'https://' if SECURE_MODE else 'http://' DOMAIN = PROTOCOL + 'localhost:5000/' INTERNAL_DOMAIN = DOMAIN API_DOMAIN = PROTOCOL + 'localhost:8000/' RESET_PASSWORD_URL = PROTOCOL + 'localhost:5000/resetpassword/' # TODO set angular reset password url -LOCAL_ANGULAR_URL = 'localhost:4200' +LOCAL_ANGULAR_DOMAIN = PROTOCOL + 'localhost:4200/' # Only used when LOCAL_MODE is True PREPRINT_PROVIDER_DOMAINS = { 'enabled': False, diff --git a/website/settings/local-ci.py b/website/settings/local-ci.py index a4d250a9792..eec0e7070e2 100644 --- a/website/settings/local-ci.py +++ b/website/settings/local-ci.py @@ -12,10 +12,12 @@ DEV_MODE = True DEBUG_MODE = True # Sets app to debug mode, turns off template caching, etc. SECURE_MODE = not DEBUG_MODE # Disable osf secure cookie +LOCAL_MODE = True # handles angular and web with different domains in local env PROTOCOL = 'https://' if SECURE_MODE else 'http://' DOMAIN = PROTOCOL + 'localhost:5000/' API_DOMAIN = PROTOCOL + 'localhost:8000/' +LOCAL_ANGULAR_DOMAIN = PROTOCOL + 'localhost:4200/' # Only used when LOCAL_MODE is True ENABLE_INSTITUTIONS = True PREPRINT_PROVIDER_DOMAINS = { diff --git a/website/settings/local-dist.py b/website/settings/local-dist.py index 01d26b420c2..fbd7f4a451e 100644 --- a/website/settings/local-dist.py +++ b/website/settings/local-dist.py @@ -11,6 +11,7 @@ DEV_MODE = True DEBUG_MODE = True # Sets app to debug mode, turns off template caching, etc. SECURE_MODE = not DEBUG_MODE # Disable osf cookie secure +LOCAL_MODE = True # handles angular and web with different domains in local env # NOTE: Internal Domains/URLs have been added to facilitate docker development environments # when localhost inside a container != localhost on the client machine/docker host. @@ -19,7 +20,7 @@ DOMAIN = PROTOCOL + 'localhost:5000/' INTERNAL_DOMAIN = DOMAIN API_DOMAIN = PROTOCOL + 'localhost:8000/' -LOCAL_ANGULAR_DOMAIN = PROTOCOL + 'localhost:4200/' +LOCAL_ANGULAR_DOMAIN = PROTOCOL + 'localhost:4200/' # Only used when LOCAL_MODE is True #WATERBUTLER_URL = 'http://localhost:7777' #WATERBUTLER_INTERNAL_URL = WATERBUTLER_URL diff --git a/website/util/__init__.py b/website/util/__init__.py index f74182300cf..70ae6644e90 100644 --- a/website/util/__init__.py +++ b/website/util/__init__.py @@ -88,7 +88,7 @@ def api_v2_url(path_str, return x # Move to api utils? -def web_url_for(view_name, _absolute=False, _internal=False, _guid=False, *args, **kwargs): +def web_url_for(view_name, _absolute=False, _internal=False, _guid=False, _angular_route=False, *args, **kwargs): """Reverse URL lookup for web routes (those that use the OsfWebRenderer). Takes the same arguments as Flask's url_for, with the addition of `_absolute`, which will make an absolute URL with the correct HTTP scheme @@ -102,6 +102,11 @@ def web_url_for(view_name, _absolute=False, _internal=False, _guid=False, *args, # We do NOT use the url_for's _external kwarg because app.config['SERVER_NAME'] alters # behavior in an unknown way (currently breaks tests). /sloria /jspies domain = website_settings.INTERNAL_DOMAIN if _internal else website_settings.DOMAIN + if website_settings.LOCAL_MODE and _angular_route: + # We use `web_url_for()` to build URLs that actually goes to the angular + # container. It is not a problem for servers since web and angular shares + # the same domain. However, we need to handle this differently locally. + domain = website_settings.LOCAL_ANGULAR_DOMAIN return urljoin(domain, url) return url From d14ee17792601e77f2d8246ca3b92d5be3c64d07 Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Fri, 24 Apr 2026 14:57:27 -0400 Subject: [PATCH 04/11] Update auth views to use updated web_url_for --- framework/auth/views.py | 21 ++++++++++----------- website/routes.py | 4 ++-- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/framework/auth/views.py b/framework/auth/views.py index 02fef28ab24..1b95bc3d6e7 100644 --- a/framework/auth/views.py +++ b/framework/auth/views.py @@ -84,7 +84,7 @@ def _reset_password_get(auth, uid=None, token=None, institutional=False): raise HTTPError(http_status.HTTP_400_BAD_REQUEST, data=error_data) # override routes.py login_url to redirect to my-projects - service_url = web_url_for('dashboard', _absolute=True) + service_url = web_url_for('dashboard', _absolute=True, _angular_route=True) return { 'uid': user_obj._id, @@ -142,7 +142,7 @@ def reset_password_post(uid=None, token=None): status.push_status_message('Password reset', kind='success', trust=False) # redirect to CAS and authenticate the user automatically with one-time verification key. return redirect(cas.get_login_url( - web_url_for('user_account', _absolute=True), + web_url_for('user_account', _absolute=True, _angular_route=True), username=user_obj.username, verification_key=user_obj.verification_key )) @@ -176,7 +176,7 @@ def forgot_password_get(auth): #overriding the routes.py sign in url to redirect to the my-projects after login context = {} - context['login_url'] = web_url_for('dashboard', _absolute=True) + context['login_url'] = web_url_for('dashboard', _absolute=True, _angular_route=True) return context @@ -324,7 +324,7 @@ def login_and_register_handler(auth, login=True, campaign=None, next_url=None, l # unlike other campaigns, institution login serves as an alternative for authentication if campaign == 'institution': if next_url is None: - next_url = web_url_for('my_projects', _absolute=True) + next_url = web_url_for('dashboard', _absolute=True, _angular_route=True) data['status_code'] = http_status.HTTP_302_FOUND if auth.logged_in: data['next_url'] = next_url @@ -391,7 +391,7 @@ def login_and_register_handler(auth, login=True, campaign=None, next_url=None, l # `/login/` or `/register/` without any parameter if auth.logged_in: data['status_code'] = http_status.HTTP_302_FOUND - data['next_url'] = web_url_for('index', _absolute=True) + data['next_url'] = web_url_for('index', _absolute=True, _angular_route=True) return data @@ -614,7 +614,7 @@ def external_login_confirm_email_get(auth, uid, token): return redirect(campaign_url) if new: status.push_status_message(language.WELCOME_MESSAGE, kind='default', jumbotron=True, trust=True, id='welcome_message') - return redirect(web_url_for('my_projects')) + return redirect(web_url_for('dashboard', _absolute=True, _angular_route=True)) # token is invalid if token not in user.email_verifications: @@ -712,10 +712,10 @@ def confirm_email_get(token, auth=None, **kwargs): status.push_status_message(language.WELCOME_MESSAGE, kind='default', jumbotron=True, trust=True, id='welcome_message') if token in auth.user.email_verifications: status.push_status_message(language.CONFIRM_ALTERNATE_EMAIL_ERROR, kind='danger', trust=True, id='alternate_email_error') - return redirect(web_url_for('my_projects')) + return redirect(web_url_for('dashboard', _absolute=True, _angular_route=True)) status.push_status_message(language.MERGE_COMPLETE, kind='success', trust=False) - return redirect(web_url_for('user_account')) + return redirect(web_url_for('user_account', _absolute=True, _angular_route=True)) try: user.confirm_email(token, merge=is_merge) @@ -1053,8 +1053,7 @@ def external_login_email_post(): fullname = session.get('auth_user_fullname', None) or form.name.data service_url = session.get('service_url', None) - # TODO: @cslzchen use user tags instead of destination - destination = 'my_projects' + destination = 'dashboard' for campaign in campaigns.get_campaigns(): if campaign != 'institution': # Handle different url encoding schemes between `furl` and `urlparse/urllib`. @@ -1203,7 +1202,7 @@ def validate_next_url(next_url): """ # allow redirection to angular locally - if settings.LOCAL_ANGULAR_URL in next_url and settings.DEBUG_MODE: + if settings.LOCAL_MODE and next_url.startswith(settings.LOCAL_ANGULAR_DOMAIN): return True # disable external domain using `//`: the browser allows `//` as a shortcut for non-protocol specific requests diff --git a/website/routes.py b/website/routes.py index ec7245fd00d..7bc8d1cab19 100644 --- a/website/routes.py +++ b/website/routes.py @@ -231,9 +231,9 @@ def sitemap_file(path): def goodbye(): # Redirect to dashboard if logged in if _get_current_user(): - return redirect(util.web_url_for('dashboard')) + return redirect(util.web_url_for('dashboard', _absolute=True, _local_angular=True)) else: - return redirect(util.web_url_for('index')) + return redirect(util.web_url_for('index', _absolute=True, _local_angular=True)) def make_url_map(app): """Set up all the routes for the OSF app. From bd582b3efa6fa770979b7f05cbf3676277da9c9e Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Fri, 24 Apr 2026 15:02:34 -0400 Subject: [PATCH 05/11] Update auth views tests --- tests/test_auth_views.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/test_auth_views.py b/tests/test_auth_views.py index 75ef5de497a..444f5788f80 100644 --- a/tests/test_auth_views.py +++ b/tests/test_auth_views.py @@ -585,13 +585,13 @@ def test_next_url_login_with_auth(self): assert data.get('next_url') == self.next_url def test_next_url_angular_login_with_auth(self): - data = login_and_register_handler(self.auth, next_url=settings.LOCAL_ANGULAR_URL) + data = login_and_register_handler(self.auth, next_url=settings.LOCAL_ANGULAR_DOMAIN) assert data.get('status_code') == http_status.HTTP_302_FOUND - assert data.get('next_url') == settings.LOCAL_ANGULAR_URL + assert data.get('next_url') == settings.LOCAL_ANGULAR_DOMAIN def test_next_url_angular_login_without_auth(self): - request.url = web_url_for('auth_login', next=settings.LOCAL_ANGULAR_URL, _absolute=True) - data = login_and_register_handler(self.no_auth, next_url=settings.LOCAL_ANGULAR_URL) + request.url = web_url_for('auth_login', next=settings.LOCAL_ANGULAR_DOMAIN, _absolute=True) + data = login_and_register_handler(self.no_auth, next_url=settings.LOCAL_ANGULAR_DOMAIN) assert data.get('status_code') == http_status.HTTP_302_FOUND assert data.get('next_url') == cas.get_login_url(request.url) @@ -838,7 +838,6 @@ def test_logout_with_no_parameter(self): assert resp.status_code == http_status.HTTP_302_FOUND assert cas.get_logout_url(self.goodbye_url) == resp.headers['Location'] - @mock.patch('framework.auth.views.settings.LOCAL_ANGULAR_URL', 'http://localhost:4200') def test_logout_with_angular_next_url_logged_in(self): angular_url = 'http://localhost:4200/' logout_url = web_url_for('auth_logout', _absolute=True, next=angular_url) @@ -846,9 +845,8 @@ def test_logout_with_angular_next_url_logged_in(self): assert resp.status_code == http_status.HTTP_302_FOUND assert cas.get_logout_url(logout_url) == resp.headers['Location'] - @mock.patch('framework.auth.views.settings.LOCAL_ANGULAR_URL', 'http://localhost:4200') def test_logout_with_angular_next_url_logged_out(self): - angular_url = 'http://localhost:4200/' + angular_url = 'http://localhost:4200/'q logout_url = web_url_for('auth_logout', _absolute=True, next=angular_url) resp = self.app.get(logout_url, auth=None) assert resp.status_code == http_status.HTTP_302_FOUND From 8635f0728f1db0bc7adaf03115c9a90330216789 Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Fri, 24 Apr 2026 16:27:28 -0400 Subject: [PATCH 06/11] Add login url builder and update logout url builder --- framework/auth/utils.py | 9 ++++++++- framework/auth/views.py | 8 +++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/framework/auth/utils.py b/framework/auth/utils.py index 8f5bbca97b9..53c7df21a70 100644 --- a/framework/auth/utils.py +++ b/framework/auth/utils.py @@ -171,8 +171,15 @@ def generate_csl_given_name(given_name, middle_names='', suffix=''): given = f'{given}, {suffix}' return given +def get_default_osf_login_url(): + """Return the default OSF login URL. + """ + next_url = web_url_for(view_name='index', _absolute=True, _angular_route=True) + return web_url_for(view_name='auth_login', _absolute=True, next=next_url) + + def get_default_osf_logout_url(): """Return the default OSF logout URL. """ - next_url = web_url_for(view_name='index', _absolute=True) + next_url = web_url_for(view_name='index', _absolute=True, _angular_route=True) return web_url_for(view_name='auth_logout', _absolute=True, next=next_url) diff --git a/framework/auth/views.py b/framework/auth/views.py index 1b95bc3d6e7..0a345fe2bf5 100644 --- a/framework/auth/views.py +++ b/framework/auth/views.py @@ -18,7 +18,7 @@ from framework.auth.core import generate_verification_key from framework.auth.decorators import block_bing_preview, collect_auth, must_be_logged_in from framework.auth.forms import ResendConfirmationForm, ForgotPasswordForm, ResetPasswordForm -from framework.auth.utils import ensure_external_identity_uniqueness, validate_recaptcha +from framework.auth.utils import ensure_external_identity_uniqueness, validate_recaptcha, get_default_osf_login_url from framework.celery_tasks.handlers import enqueue_task from framework.exceptions import HTTPError from framework.flask import redirect # VOL-aware redirect @@ -389,9 +389,11 @@ def login_and_register_handler(auth, login=True, campaign=None, next_url=None, l data['next_url'] = request.url else: # `/login/` or `/register/` without any parameter + data['status_code'] = http_status.HTTP_302_FOUND if auth.logged_in: - data['status_code'] = http_status.HTTP_302_FOUND - data['next_url'] = web_url_for('index', _absolute=True, _angular_route=True) + data['next_url'] = web_url_for('dashboard', _absolute=True, _angular_route=True) + else: + data['next_url'] = cas.get_login_url(get_default_osf_login_url()) return data From 7ff81c878b057690694fd3e01e8f011315249e9b Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Fri, 24 Apr 2026 16:31:05 -0400 Subject: [PATCH 07/11] Fix some tests --- tests/test_auth.py | 2 +- tests/test_auth_basic_auth.py | 2 +- tests/test_auth_views.py | 2 +- tests/test_campaigns.py | 2 +- tests/test_webtests.py | 22 ++++++++-------------- 5 files changed, 12 insertions(+), 18 deletions(-) diff --git a/tests/test_auth.py b/tests/test_auth.py index 487759603c1..240a0cb317a 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -96,7 +96,7 @@ def test_confirm_email(self): res = self.app.resolve_redirect(res) assert res.status_code == 302 - assert '/my-projects/' == urlparse(res.location).path + assert '/dashboard/' == urlparse(res.location).path # assert len(get_session()['status']) == 1 def test_get_user_by_id(self): diff --git a/tests/test_auth_basic_auth.py b/tests/test_auth_basic_auth.py index e1b9d6e5b8d..86b1d5fbec1 100644 --- a/tests/test_auth_basic_auth.py +++ b/tests/test_auth_basic_auth.py @@ -99,4 +99,4 @@ def test_expired_cookie(self): self.app.set_cookie(settings.COOKIE_NAME, str(cookie)) res = self.app.get(self.reachable_url) assert res.status_code == 302 - assert '/login/' == res.location + assert 'http://localhost:5000/logout/?next=http://localhost:4200/' == res.location diff --git a/tests/test_auth_views.py b/tests/test_auth_views.py index 444f5788f80..2e1c87164d1 100644 --- a/tests/test_auth_views.py +++ b/tests/test_auth_views.py @@ -846,7 +846,7 @@ def test_logout_with_angular_next_url_logged_in(self): assert cas.get_logout_url(logout_url) == resp.headers['Location'] def test_logout_with_angular_next_url_logged_out(self): - angular_url = 'http://localhost:4200/'q + angular_url = 'http://localhost:4200/' logout_url = web_url_for('auth_logout', _absolute=True, next=angular_url) resp = self.app.get(logout_url, auth=None) assert resp.status_code == http_status.HTTP_302_FOUND diff --git a/tests/test_campaigns.py b/tests/test_campaigns.py index 221ce03f8cf..315e4b7acbd 100644 --- a/tests/test_campaigns.py +++ b/tests/test_campaigns.py @@ -238,7 +238,7 @@ def setUp(self): super().setUp() self.url_login = web_url_for('auth_login', campaign='institution') self.url_register = web_url_for('auth_register', campaign='institution') - self.service_url = web_url_for('my_projects', _absolute=True) + self.service_url = web_url_for('dashboard', _absolute=True, _anchor=True) # go to CAS institution login page if not logged in def test_institution_not_logged_in(self): diff --git a/tests/test_webtests.py b/tests/test_webtests.py index 64c5669f3e9..3524aeb03fa 100644 --- a/tests/test_webtests.py +++ b/tests/test_webtests.py @@ -87,27 +87,23 @@ def test_is_redirected_to_cas_if_not_logged_in_at_login_page(self): location = res.headers.get('Location') assert 'login?service=' in location - def test_is_redirected_to_myprojects_if_already_logged_in_at_login_page(self): + def test_is_redirected_to_dashboard_if_already_logged_in_at_login_page(self): res = self.app.get('/login/', auth=self.user.auth) assert res.status_code == 302 - assert 'my-projects' in res.headers.get('Location') + assert 'dashboard' in res.headers.get('Location') def test_register_page(self): res = self.app.get('/register/') assert res.status_code == 200 - def test_is_redirected_to_myprojects_if_already_logged_in_at_register_page(self): + def test_is_redirected_to_dashboard_if_already_logged_in_at_register_page(self): res = self.app.get('/register/', auth=self.user.auth) assert res.status_code == 302 - assert 'my-projects' in res.headers.get('Location') + assert 'dashboard' in res.headers.get('Location') def test_sees_projects_in_her_dashboard(self): - # the user already has a project - project = ProjectFactory(creator=self.user) - project.add_contributor(self.user) - project.save() - res = self.app.get('/my-projects/', auth=self.user.auth) - assert 'Projects' in res.text # Projects heading + # Deprecated test, dashboard and my-projects are angular pages + pass def test_does_not_see_osffiles_in_user_addon_settings(self): res = self.app.get('/settings/addons/', auth=self.auth, follow_redirects=True) @@ -123,10 +119,8 @@ def test_sees_osffiles_in_project_addon_settings(self): assert 'OSF Storage' in res.text def test_sees_correct_title_on_dashboard(self): - # User goes to dashboard - res = self.app.get('/my-projects/', auth=self.auth, follow_redirects=True) - title = res.html.title.string - assert 'OSF | My Projects' == title + # Deprecated test, dashboard and my-projects are angular pages + pass def test_can_see_make_public_button_if_admin(self): # User is a contributor on a project From e2647451fd08e109c330279109745af5ebbb1bc6 Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Mon, 27 Apr 2026 13:30:13 -0400 Subject: [PATCH 08/11] Update institution's CAS login url in admin --- osf/models/institution.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/osf/models/institution.py b/osf/models/institution.py index 379f301a966..87cb12eed05 100644 --- a/osf/models/institution.py +++ b/osf/models/institution.py @@ -216,12 +216,16 @@ def banner_path(self): def cas_login_url(self): if self.delegation_protocol == IntegrationType.NONE.value: return None - if 'localhost' in website_settings.DOMAIN: - next_param = quote(website_settings.PROTOCOL + website_settings.LOCAL_ANGULAR_URL, safe='') - else: - next_param = quote(website_settings.DOMAIN, safe='') - service_url = quote(f'{website_settings.DOMAIN}login?next={next_param}', safe='') - return f'{website_settings.CAS_SERVER_URL}/login?campaign=institution&institutionId={self._id}&service={service_url}' + # Note: admin app can't use `web_url_for()` due to out of context + next_url_param = quote(website_settings.DOMAIN, safe='') + service_url_param = quote(f'{website_settings.DOMAIN}login?next={next_url_param}', safe='') + institution_id_param = quote(self._id, safe='') + return ( + f'{website_settings.CAS_SERVER_URL}/login' + f'?campaign=institution' + f'&institutionId={institution_id_param}' + f'&service={service_url_param}' + ) def update_search(self): from website.search.search import update_institution From 843d9a15498934ad9c664b27bb193c17970ad182 Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Mon, 27 Apr 2026 20:55:33 -0400 Subject: [PATCH 09/11] More auth view update and more tests fix --- framework/auth/views.py | 8 +++----- tests/test_auth_views.py | 25 +++++++++++++++---------- tests/test_campaigns.py | 2 +- tests/test_webtests.py | 5 ++--- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/framework/auth/views.py b/framework/auth/views.py index 0a345fe2bf5..debbedb0d55 100644 --- a/framework/auth/views.py +++ b/framework/auth/views.py @@ -18,7 +18,7 @@ from framework.auth.core import generate_verification_key from framework.auth.decorators import block_bing_preview, collect_auth, must_be_logged_in from framework.auth.forms import ResendConfirmationForm, ForgotPasswordForm, ResetPasswordForm -from framework.auth.utils import ensure_external_identity_uniqueness, validate_recaptcha, get_default_osf_login_url +from framework.auth.utils import ensure_external_identity_uniqueness, validate_recaptcha from framework.celery_tasks.handlers import enqueue_task from framework.exceptions import HTTPError from framework.flask import redirect # VOL-aware redirect @@ -389,11 +389,9 @@ def login_and_register_handler(auth, login=True, campaign=None, next_url=None, l data['next_url'] = request.url else: # `/login/` or `/register/` without any parameter - data['status_code'] = http_status.HTTP_302_FOUND if auth.logged_in: - data['next_url'] = web_url_for('dashboard', _absolute=True, _angular_route=True) - else: - data['next_url'] = cas.get_login_url(get_default_osf_login_url()) + data['status_code'] = http_status.HTTP_302_FOUND + data['next_url'] = web_url_for('dashboard', _absolute=True, _angular_route=True) return data diff --git a/tests/test_auth_views.py b/tests/test_auth_views.py index 2e1c87164d1..23376e65995 100644 --- a/tests/test_auth_views.py +++ b/tests/test_auth_views.py @@ -551,32 +551,32 @@ def setUp(self): self.no_auth = Auth() self.user_auth = AuthUserFactory() self.auth = Auth(user=self.user_auth) - self.next_url = web_url_for('my_projects', _absolute=True) + self.next_url = web_url_for('dashboard', _absolute=True, _angular_route=True) self.invalid_campaign = 'invalid_campaign' def test_osf_login_with_auth(self): # login: user with auth data = login_and_register_handler(self.auth) assert data.get('status_code') == http_status.HTTP_302_FOUND - assert data.get('next_url') == web_url_for('my_projects', _absolute=True) + assert data.get('next_url') == web_url_for('dashboard', _absolute=True, _angular_route=True) def test_osf_login_without_auth(self): # login: user without auth data = login_and_register_handler(self.no_auth) assert data.get('status_code') == http_status.HTTP_302_FOUND - assert data.get('next_url') == web_url_for('my_projects', _absolute=True) + assert data.get('next_url') == web_url_for('dashboard', _absolute=True, _angular_route=True) def test_osf_register_with_auth(self): # register: user with auth data = login_and_register_handler(self.auth, login=False) assert data.get('status_code') == http_status.HTTP_302_FOUND - assert data.get('next_url') == web_url_for('my_projects', _absolute=True) + assert data.get('next_url') == web_url_for('dashboard', _absolute=True, _angular_route=True) def test_osf_register_without_auth(self): # register: user without auth data = login_and_register_handler(self.no_auth, login=False) assert data.get('status_code') == http_status.HTTP_200_OK - assert data.get('next_url') == web_url_for('my_projects', _absolute=True) + assert data.get('next_url') == web_url_for('dashboard', _absolute=True, _angular_route=True) def test_next_url_login_with_auth(self): # next_url login: user with auth @@ -621,14 +621,16 @@ def test_institution_login_with_auth(self): # institution login: user with auth data = login_and_register_handler(self.auth, campaign='institution') assert data.get('status_code') == http_status.HTTP_302_FOUND - assert data.get('next_url') == web_url_for('my_projects', _absolute=True) + assert data.get('next_url') == web_url_for('dashboard', _absolute=True, _angular_route=True) def test_institution_login_without_auth(self): # institution login: user without auth data = login_and_register_handler(self.no_auth, campaign='institution') assert data.get('status_code') == http_status.HTTP_302_FOUND - assert data.get('next_url') == cas.get_login_url(web_url_for('my_projects', _absolute=True), - campaign='institution') + assert data.get('next_url') == cas.get_login_url( + web_url_for('my_projects', _absolute=True, _angular_route=True), + campaign='institution' + ) def test_institution_login_next_url_with_auth(self): # institution login: user with auth and next url @@ -646,13 +648,16 @@ def test_institution_register_with_auth(self): # institution register: user with auth data = login_and_register_handler(self.auth, login=False, campaign='institution') assert data.get('status_code') == http_status.HTTP_302_FOUND - assert data.get('next_url') == web_url_for('my_projects', _absolute=True) + assert data.get('next_url') == web_url_for('dashboard', _absolute=True, _angular_route=True) def test_institution_register_without_auth(self): # institution register: user without auth data = login_and_register_handler(self.no_auth, login=False, campaign='institution') assert data.get('status_code') == http_status.HTTP_302_FOUND - assert data.get('next_url') == cas.get_login_url(web_url_for('my_projects', _absolute=True), campaign='institution') + assert data.get('next_url') == cas.get_login_url( + web_url_for('dashboard', _absolute=True, _angular_route=True), + campaign='institution' + ) def test_campaign_login_with_auth(self): for campaign in get_campaigns(): diff --git a/tests/test_campaigns.py b/tests/test_campaigns.py index 315e4b7acbd..49838c6244e 100644 --- a/tests/test_campaigns.py +++ b/tests/test_campaigns.py @@ -238,7 +238,7 @@ def setUp(self): super().setUp() self.url_login = web_url_for('auth_login', campaign='institution') self.url_register = web_url_for('auth_register', campaign='institution') - self.service_url = web_url_for('dashboard', _absolute=True, _anchor=True) + self.service_url = web_url_for('dashboard', _absolute=True, _angular_route=True) # go to CAS institution login page if not logged in def test_institution_not_logged_in(self): diff --git a/tests/test_webtests.py b/tests/test_webtests.py index 3524aeb03fa..97efe3ca715 100644 --- a/tests/test_webtests.py +++ b/tests/test_webtests.py @@ -82,10 +82,9 @@ def test_can_see_profile_url(self): # `GET /login/` without parameters is redirected to `/my-projects/` page which has `@must_be_logged_in` decorator # if user is not logged in, she/he is further redirected to CAS login page def test_is_redirected_to_cas_if_not_logged_in_at_login_page(self): - res = self.app.resolve_redirect(self.app.get('/login/')) + res = self.app.get('/login/') assert res.status_code == 302 - location = res.headers.get('Location') - assert 'login?service=' in location + assert 'login?service=' in res.headers.get('Location') def test_is_redirected_to_dashboard_if_already_logged_in_at_login_page(self): res = self.app.get('/login/', auth=self.user.auth) From ef31d07318d86d141c75fc201c406e89db259a44 Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Mon, 27 Apr 2026 21:31:04 -0400 Subject: [PATCH 10/11] Fix the last couple of failures --- tests/test_auth_views.py | 2 +- tests/test_webtests.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_auth_views.py b/tests/test_auth_views.py index 23376e65995..965501d2e03 100644 --- a/tests/test_auth_views.py +++ b/tests/test_auth_views.py @@ -628,7 +628,7 @@ def test_institution_login_without_auth(self): data = login_and_register_handler(self.no_auth, campaign='institution') assert data.get('status_code') == http_status.HTTP_302_FOUND assert data.get('next_url') == cas.get_login_url( - web_url_for('my_projects', _absolute=True, _angular_route=True), + web_url_for('dashboard', _absolute=True, _angular_route=True), campaign='institution' ) diff --git a/tests/test_webtests.py b/tests/test_webtests.py index 97efe3ca715..037aca0a137 100644 --- a/tests/test_webtests.py +++ b/tests/test_webtests.py @@ -79,12 +79,12 @@ def test_can_see_profile_url(self): res = self.app.get(self.user.url, follow_redirects=True) assert self.user.url in res.text - # `GET /login/` without parameters is redirected to `/my-projects/` page which has `@must_be_logged_in` decorator - # if user is not logged in, she/he is further redirected to CAS login page + # `GET /login/` (legacy BE endpoint) without parameters is redirected to `/dashboard/` (angular FE endpoint). + # It's impossible to test external redirects in tests, and it is angular's job to redirect correctly to CAS login. def test_is_redirected_to_cas_if_not_logged_in_at_login_page(self): res = self.app.get('/login/') assert res.status_code == 302 - assert 'login?service=' in res.headers.get('Location') + assert 'dashboard' in res.headers.get('Location') def test_is_redirected_to_dashboard_if_already_logged_in_at_login_page(self): res = self.app.get('/login/', auth=self.user.auth) From 88d452e7efc85a3005efc68e24b3c2a5aedf3618 Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Tue, 28 Apr 2026 16:13:19 -0400 Subject: [PATCH 11/11] Fix goodbye route --- website/routes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/routes.py b/website/routes.py index 7bc8d1cab19..af14a916183 100644 --- a/website/routes.py +++ b/website/routes.py @@ -231,9 +231,9 @@ def sitemap_file(path): def goodbye(): # Redirect to dashboard if logged in if _get_current_user(): - return redirect(util.web_url_for('dashboard', _absolute=True, _local_angular=True)) + return redirect(util.web_url_for('dashboard', _absolute=True, _angular_route=True)) else: - return redirect(util.web_url_for('index', _absolute=True, _local_angular=True)) + return redirect(util.web_url_for('index', _absolute=True, _angular_route=True)) def make_url_map(app): """Set up all the routes for the OSF app.