v0.163.0
  1from __future__ import annotations
  2
  3from typing import TYPE_CHECKING, Any
  4
  5from plain.auth import login, logout
  6from plain.auth.views import AuthView
  7from plain.http import RedirectResponse, Response
  8from plain.runtime import settings
  9from plain.templates.views import FormView, TemplateView
 10from plain.urls import reverse, reverse_lazy
 11from plain.views import View
 12
 13from .forms import LoginLinkForm
 14from .links import (
 15    LoginLinkChanged,
 16    LoginLinkExpired,
 17    LoginLinkInvalid,
 18    get_link_token_user,
 19)
 20
 21if TYPE_CHECKING:
 22    from plain.http import Request
 23
 24
 25def redirect_to_next_url(request: Request, default: str = "/") -> RedirectResponse:
 26    """Redirect to the "next" query param, or the default when it's missing,
 27    empty, or an external URL (which RedirectResponse refuses)."""
 28    next_url = request.query_params.get("next") or default
 29    try:
 30        return RedirectResponse(next_url, status_code=302)
 31    except ValueError:
 32        return RedirectResponse(default, status_code=302)
 33
 34
 35class LoginLinkFormView(AuthView, FormView[LoginLinkForm]):
 36    form_class = LoginLinkForm
 37    success_url = reverse_lazy("loginlink:sent")
 38
 39    def get(self) -> Response:
 40        # Redirect if the user is already logged in. The form is never
 41        # validated on a GET, so "next" comes from the query string.
 42        if self.user:
 43            return redirect_to_next_url(self.request)
 44
 45        return super().get()
 46
 47    def form_valid(self, form: LoginLinkForm) -> Response:
 48        form.maybe_send_link(self.request)
 49        return super().form_valid(form)
 50
 51    def get_success_url(self, form: LoginLinkForm) -> str:
 52        if next_url := form.cleaned_data.get("next"):
 53            # Keep the next URL in the query string so the sent
 54            # view can redirect to it if reloaded and logged in already.
 55            return f"{self.success_url}?next={next_url}"
 56        else:
 57            return self.success_url
 58
 59
 60class LoginLinkSentView(AuthView, TemplateView):
 61    template_name = "loginlink/sent.html"
 62
 63    def get(self) -> Response:
 64        # Redirect if the user is already logged in
 65        if self.user:
 66            return redirect_to_next_url(self.request)
 67
 68        return super().get()
 69
 70
 71class LoginLinkFailedView(TemplateView):
 72    template_name = "loginlink/failed.html"
 73
 74    def get_template_context(self) -> dict[str, Any]:
 75        context = super().get_template_context()
 76        context["error"] = self.request.query_params.get("error")
 77        context["login_url"] = reverse(settings.AUTH_LOGIN_URL)
 78        return context
 79
 80
 81class LoginLinkLoginView(AuthView, View):
 82    success_url = "/"
 83
 84    def get(self) -> Response:
 85        # If they're logged in, log them out and process the link again
 86        if self.user:
 87            logout(self.request)
 88
 89        token = self.url_kwargs["token"]
 90
 91        try:
 92            user = get_link_token_user(token)
 93        except LoginLinkExpired:
 94            return RedirectResponse(
 95                reverse("loginlink:failed") + "?error=expired", status_code=302
 96            )
 97        except LoginLinkInvalid:
 98            return RedirectResponse(
 99                reverse("loginlink:failed") + "?error=invalid", status_code=302
100            )
101        except LoginLinkChanged:
102            return RedirectResponse(
103                reverse("loginlink:failed") + "?error=changed", status_code=302
104            )
105
106        login(self.request, user)
107
108        return redirect_to_next_url(self.request, default=self.success_url)