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