Browse Source

Redirect back to "source" page after logging in

Previously, logging in would always send you to the home page. Now the
user will be sent back to whichever page they clicked the "Log in" link
from. Note that the destination is not validated, other than ensuring
that it starts with a "/" to prevent redirects to different sites.
merge-requests/74/head
Deimos 5 years ago
parent
commit
d602eb5611
  1. 4
      tildes/scss/themes/_theme_base.scss
  2. 7
      tildes/tildes/jinja.py
  3. 1
      tildes/tildes/templates/intercooler/login_two_factor.jinja2
  4. 1
      tildes/tildes/templates/login.jinja2
  5. 9
      tildes/tildes/templates/macros/user.jinja2
  6. 37
      tildes/tildes/views/login.py

4
tildes/scss/themes/_theme_base.scss

@ -366,6 +366,10 @@
@include theme-special-label(map-get($theme, "topic-tag-spoiler"), $is-light); @include theme-special-label(map-get($theme, "topic-tag-spoiler"), $is-light);
} }
.link-no-visited-color:visited {
color: map-get($theme, "link");
}
.logged-in-user-username, .logged-in-user-username,
.logged-in-user-username:visited { .logged-in-user-username:visited {
color: map-get($theme, "foreground-primary"); color: map-get($theme, "foreground-primary");

7
tildes/tildes/jinja.py

@ -4,6 +4,7 @@
"""Contains configuration, functions, etc. for the Jinja template system.""" """Contains configuration, functions, etc. for the Jinja template system."""
from typing import Any from typing import Any
from urllib.parse import quote_plus
from pyramid.config import Configurator from pyramid.config import Configurator
@ -28,6 +29,11 @@ def is_topic(item: Any) -> bool:
return isinstance(item, Topic) return isinstance(item, Topic)
def do_quote_plus(string: str) -> str:
"""Escape the string using the urllib quote_plus function."""
return quote_plus(string)
def includeme(config: Configurator) -> None: def includeme(config: Configurator) -> None:
"""Configure Jinja2 template renderer.""" """Configure Jinja2 template renderer."""
settings = config.get_settings() settings = config.get_settings()
@ -39,6 +45,7 @@ def includeme(config: Configurator) -> None:
settings["jinja2.filters"] = { settings["jinja2.filters"] = {
"adaptive_date": adaptive_date, "adaptive_date": adaptive_date,
"ago": descriptive_timedelta, "ago": descriptive_timedelta,
"quote_plus": do_quote_plus,
} }
# add custom jinja tests # add custom jinja tests

1
tildes/tildes/templates/intercooler/login_two_factor.jinja2

@ -5,6 +5,7 @@
<form class="form-narrow" method="post" autocomplete="off" action="/login_two_factor" data-ic-post-to="/login_two_factor"> <form class="form-narrow" method="post" autocomplete="off" action="/login_two_factor" data-ic-post-to="/login_two_factor">
<input type="hidden" name="csrf_token" value="{{ get_csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ get_csrf_token() }}">
<input type="hidden" name="from_url" value="{{ from_url }}">
{% if keep %} {% if keep %}
<input type="hidden" name="keep" value="on"> <input type="hidden" name="keep" value="on">
{% endif %} {% endif %}

1
tildes/tildes/templates/login.jinja2

@ -10,6 +10,7 @@
{% block content %} {% block content %}
<form class="form-narrow form-login" method="post" data-ic-post-to="/login" data-ic-target="closest main"> <form class="form-narrow form-login" method="post" data-ic-post-to="/login" data-ic-target="closest main">
<input type="hidden" name="csrf_token" value="{{ get_csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ get_csrf_token() }}">
<input type="hidden" name="from_url" value="{{ from_url }}">
<div class="form-group"> <div class="form-group">
<label class="form-label col-4" for="username">Username</label> <label class="form-label col-4" for="username">Username</label>

9
tildes/tildes/templates/macros/user.jinja2

@ -25,8 +25,15 @@
{% endtrans %} {% endtrans %}
</a> </a>
{% endif %} {% endif %}
{# Only show the "Log in" link if we're not already on the login page #}
{% elif not request.matched_route or request.matched_route.name != "login" %}
<a class="text-small link-no-visited-color"
{% if request.path_qs == "/" %}
href="/login"
{% else %} {% else %}
<a class="text-small" href="/login">Log in</a>
href="/login?from_url={{ request.path_qs|quote_plus }}"
{% endif %}
>Log in</a>
{% endif %} {% endif %}
</div> </div>
{% endmacro %} {% endmacro %}

37
tildes/tildes/views/login.py

@ -3,7 +3,10 @@
"""Views related to logging in/out.""" """Views related to logging in/out."""
from urllib.parse import unquote_plus
from marshmallow.fields import String from marshmallow.fields import String
from mypy_extensions import NoReturn
from pyramid.httpexceptions import HTTPFound, HTTPUnauthorized, HTTPUnprocessableEntity from pyramid.httpexceptions import HTTPFound, HTTPUnauthorized, HTTPUnprocessableEntity
from pyramid.renderers import render_to_response from pyramid.renderers import render_to_response
from pyramid.request import Request from pyramid.request import Request
@ -23,15 +26,16 @@ from tildes.views.decorators import not_logged_in, rate_limit_view
@view_config( @view_config(
route_name="login", renderer="login.jinja2", permission=NO_PERMISSION_REQUIRED route_name="login", renderer="login.jinja2", permission=NO_PERMISSION_REQUIRED
) )
@use_kwargs({"from_url": String(missing="")})
@not_logged_in @not_logged_in
def get_login(request: Request) -> dict:
def get_login(request: Request, from_url: str) -> dict:
"""Display the login form.""" """Display the login form."""
# pylint: disable=unused-argument # pylint: disable=unused-argument
return {}
return {"from_url": unquote_plus(from_url)}
def finish_login(request: Request, user: User) -> None:
"""Save the user ID into session."""
def finish_login(request: Request, user: User, redirect_url: str) -> HTTPFound:
"""Save the user ID into session and return a redirect to appropriate page."""
# Username/password were correct - attach the user_id to the session # Username/password were correct - attach the user_id to the session
remember(request, user.user_id) remember(request, user.user_id)
@ -45,6 +49,12 @@ def finish_login(request: Request, user: User) -> None:
request.user = user request.user = user
request.db_session.add(Log(LogEventType.USER_LOG_IN, request)) request.db_session.add(Log(LogEventType.USER_LOG_IN, request))
# only use redirect_url if it's a relative url, so we can't redirect to other sites
if redirect_url.startswith("/"):
return HTTPFound(location=redirect_url)
return HTTPFound(location="/")
@view_config( @view_config(
route_name="login", request_method="POST", permission=NO_PERMISSION_REQUIRED route_name="login", request_method="POST", permission=NO_PERMISSION_REQUIRED
@ -54,9 +64,12 @@ def finish_login(request: Request, user: User) -> None:
only=("username", "password"), context={"username_trim_whitespace": True} only=("username", "password"), context={"username_trim_whitespace": True}
) )
) )
@use_kwargs({"from_url": String(missing="")})
@not_logged_in @not_logged_in
@rate_limit_view("login") @rate_limit_view("login")
def post_login(request: Request, username: str, password: str) -> HTTPFound:
def post_login(
request: Request, username: str, password: str, from_url: str
) -> Response:
"""Process a log in request.""" """Process a log in request."""
incr_counter("logins") incr_counter("logins")
@ -88,13 +101,11 @@ def post_login(request: Request, username: str, password: str) -> HTTPFound:
request.session["two_factor_username"] = username request.session["two_factor_username"] = username
return render_to_response( return render_to_response(
"tildes:templates/intercooler/login_two_factor.jinja2", "tildes:templates/intercooler/login_two_factor.jinja2",
{"keep": request.params.get("keep")},
{"keep": request.params.get("keep"), "from_url": from_url},
request=request, request=request,
) )
finish_login(request, user)
raise HTTPFound(location="/")
raise finish_login(request, user, from_url)
@view_config( @view_config(
@ -104,8 +115,8 @@ def post_login(request: Request, username: str, password: str) -> HTTPFound:
) )
@not_logged_in @not_logged_in
@rate_limit_view("login_two_factor") @rate_limit_view("login_two_factor")
@use_kwargs({"code": String()})
def post_login_two_factor(request: Request, code: str) -> Response:
@use_kwargs({"code": String(), "from_url": String(missing="")})
def post_login_two_factor(request: Request, code: str, from_url: str) -> NoReturn:
"""Process a log in request with 2FA.""" """Process a log in request with 2FA."""
# Look up the user for the supplied username # Look up the user for the supplied username
user = ( user = (
@ -117,9 +128,7 @@ def post_login_two_factor(request: Request, code: str) -> Response:
if user.is_correct_two_factor_code(code): if user.is_correct_two_factor_code(code):
del request.session["two_factor_username"] del request.session["two_factor_username"]
finish_login(request, user)
raise HTTPFound(location="/")
raise finish_login(request, user, from_url)
else: else:
raise HTTPUnauthorized(body="Invalid code, please try again.") raise HTTPUnauthorized(body="Invalid code, please try again.")

Loading…
Cancel
Save