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