Plain is headed towards 1.0! Subscribe for development updates →

 1from __future__ import annotations
 2
 3from typing import TYPE_CHECKING
 4
 5from plain.auth import get_user_model
 6from plain.signing import BadSignature, SignatureExpired
 7from plain.urls import reverse
 8
 9from . import signing
10
11if TYPE_CHECKING:
12    from plain.http import Request
13    from plain.models import Model
14
15
16class LoginLinkExpired(Exception):
17    pass
18
19
20class LoginLinkInvalid(Exception):
21    pass
22
23
24class LoginLinkChanged(Exception):
25    pass
26
27
28def generate_link_url(
29    *, request: Request, user: Model, email: str, expires_in: int
30) -> str:
31    """
32    Generate a login link using both the user's ID
33    and email address, so links break if the user email changes or is assigned to another user.
34    """
35    token = signing.dumps({"user_id": user.id, "email": email}, expires_in=expires_in)
36
37    return request.build_absolute_uri(reverse("loginlink:login", token))
38
39
40def get_link_token_user(token: str) -> Model:
41    """
42    Validate a link token and get the user from it.
43    """
44    try:
45        signed_data = signing.loads(token)
46    except SignatureExpired:
47        raise LoginLinkExpired()
48    except BadSignature:
49        raise LoginLinkInvalid()
50
51    user_model = get_user_model()
52    user_id = signed_data["user_id"]
53    email = signed_data["email"]
54
55    try:
56        return user_model.query.get(id=user_id, email__iexact=email)
57    except user_model.DoesNotExist:
58        raise LoginLinkChanged()