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)