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 RedirectResponse, Response
 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 RedirectResponse(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            return RedirectResponse(self.request.query_params.get("next", "/"))
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(AuthView, 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 RedirectResponse(reverse("loginlink:failed") + "?error=expired")
82        except LoginLinkInvalid:
83            return RedirectResponse(reverse("loginlink:failed") + "?error=invalid")
84        except LoginLinkChanged:
85            return RedirectResponse(reverse("loginlink:failed") + "?error=changed")
86
87        login(self.request, user)
88
89        if next_url := self.request.query_params.get("next"):
90            return RedirectResponse(next_url)
91
92        return RedirectResponse(self.success_url)