Plain is headed towards 1.0! Subscribe for development updates →

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