diff --git a/funnel/views/api/oauth.py b/funnel/views/api/oauth.py index f85f602d6..9e26a4c09 100644 --- a/funnel/views/api/oauth.py +++ b/funnel/views/api/oauth.py @@ -25,11 +25,7 @@ from ...registry import resource_registry from ...typing import ReturnView from ...utils import abort_null, make_redirect_url -from ..login_session import ( - reload_for_cookies, - requires_client_login, - requires_login_no_message, -) +from ..login_session import reload_for_cookies, requires_client_login, requires_login from .resource import get_userinfo @@ -175,7 +171,7 @@ def oauth_auth_error( @app.route('/api/1/auth', methods=['GET', 'POST']) @reload_for_cookies -@requires_login_no_message +@requires_login('') def oauth_authorize() -> ReturnView: """Provide authorization endpoint for OAuth2 server.""" form = forms.Form() diff --git a/funnel/views/login.py b/funnel/views/login.py index e3d848e58..ef22781a7 100644 --- a/funnel/views/login.py +++ b/funnel/views/login.py @@ -661,7 +661,7 @@ def account_merge() -> ReturnView: # 3. Redirect user to `app` /login/hasjob?code={code} # 2. `app` /login/hasjob does: -# 1. Ask user to login if required (@requires_login_no_message) +# 1. Ask user to login if required (@requires_login('')) # 2. Verify signature of code # 3. Create a timestamped token using (nonce, user_session.buid) # 4. Redirect user to `hasjobapp` /login/callback?token={token} @@ -703,7 +703,7 @@ def hasjob_login(cookietest: bool = False) -> ReturnView: # @app.route('/login/hasjob') # @reload_for_cookies -# @requires_login_no_message # 1. Ensure user login +# @requires_login('') # 1. Ensure user login # @requestargs('code') # def login_hasjob(code): # """Process a request for login initiated from Hasjob.""" diff --git a/funnel/views/login_session.py b/funnel/views/login_session.py index 09a693507..448b78ea8 100644 --- a/funnel/views/login_session.py +++ b/funnel/views/login_session.py @@ -4,7 +4,7 @@ from datetime import timedelta from functools import wraps -from typing import Callable, Optional, Type, Union +from typing import Callable, Optional, Type, Union, overload import geoip2.errors import itsdangerous @@ -23,7 +23,7 @@ ) from furl import furl -from baseframe import _, statsd +from baseframe import _, __, statsd from baseframe.forms import render_form from coaster.auth import add_auth_attribute, current_auth, request_has_auth from coaster.utils import utcnow @@ -440,7 +440,7 @@ def decorator(f: Callable[P, T]) -> Callable[P, Union[T, ReturnResponse]]: def wrapper(*args: P.args, **kwargs: P.kwargs) -> Union[T, ReturnResponse]: """Validate user rights in a view.""" if not current_auth.is_authenticated: - flash(_("You need to be logged in for that page"), 'info') + flash(_("Confirm your phone number to continue"), 'info') return render_redirect( url_for('login', next=get_current_url()), 302 if request.method == 'GET' else 303, @@ -460,43 +460,64 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> Union[T, ReturnResponse]: return decorator -def requires_login(f: Callable[P, T]) -> Callable[P, Union[T, ReturnResponse]]: - """Decorate a view to require login.""" +@overload +def requires_login( + __p: str, +) -> Callable[[Callable[P, T]], Callable[P, Union[T, ReturnResponse]]]: + ... - @wraps(f) - def wrapper(*args: P.args, **kwargs: P.kwargs) -> Union[T, ReturnResponse]: - add_auth_attribute('login_required', True) - if not current_auth.is_authenticated: - flash(_("You need to be logged in for that page"), 'info') - return render_redirect( - url_for('login', next=get_current_url()), - 302 if request.method == 'GET' else 303, - ) - return f(*args, **kwargs) - return wrapper +@overload +def requires_login(__p: Callable[P, T]) -> Callable[P, Union[T, ReturnResponse]]: + ... -def requires_login_no_message( - f: Callable[P, T] -) -> Callable[P, Union[T, ReturnResponse]]: +def requires_login( + __p: Union[str, Callable[P, T]] +) -> Union[ + Callable[[Callable[P, T]], Callable[P, Union[T, ReturnResponse]]], + Callable[P, Union[T, ReturnResponse]], +]: """ - Decorate a view to require login, without displaying a friendly message. + Decorate a view to require login, with a customisable message. - Used on views where the user is informed in advance that login is required. + Usage:: + + @requires_login + def view_requiring_login(): + ... + + @requires_login(__("Message to be shown")) + def view_requiring_login_with_custom_message(): + ... + + @requires_login('') + def view_requiring_login_with_no_message(): + ... """ + if callable(__p): + message = __("You need to be logged in for that page") + else: + message = __p - @wraps(f) - def wrapper(*args: P.args, **kwargs: P.kwargs) -> Union[T, ReturnResponse]: - add_auth_attribute('login_required', True) - if not current_auth.is_authenticated: - return render_redirect( - url_for('login', next=get_current_url()), - 302 if request.method == 'GET' else 303, - ) - return f(*args, **kwargs) + def decorator(f: Callable[P, T]) -> Callable[P, Union[T, ReturnResponse]]: + @wraps(f) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> Union[T, ReturnResponse]: + add_auth_attribute('login_required', True) + if not current_auth.is_authenticated: + if message: # Setting an empty message will disable it + flash(message, 'info') + return render_redirect( + url_for('login', next=get_current_url()), + 302 if request.method == 'GET' else 303, + ) + return f(*args, **kwargs) - return wrapper + return wrapper + + if callable(__p): + return decorator(__p) + return decorator def save_sudo_preference_context() -> None: diff --git a/funnel/views/notification_preferences.py b/funnel/views/notification_preferences.py index 8081594a5..623e64318 100644 --- a/funnel/views/notification_preferences.py +++ b/funnel/views/notification_preferences.py @@ -66,7 +66,9 @@ class AccountNotificationView(ClassView): current_section = 'account' @route('', endpoint='notification_preferences') - @requires_login + @requires_login( + __("Your phone number or email address is required to change your preferences") + ) @render_with('notification_preferences.html.jinja2') def notification_preferences(self) -> ReturnRenderWith: """View for notification preferences."""