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)