django-solomon 0.2.0__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_solomon/backends.py +16 -0
- django_solomon/migrations/0002_magiclink_ip_address.py +18 -0
- django_solomon/migrations/0003_add_unique_constraint_auth_user_email.py +43 -0
- django_solomon/models.py +3 -2
- django_solomon/utilities.py +69 -0
- django_solomon/views.py +7 -2
- {django_solomon-0.2.0.dist-info → django_solomon-0.4.0.dist-info}/METADATA +30 -18
- {django_solomon-0.2.0.dist-info → django_solomon-0.4.0.dist-info}/RECORD +10 -8
- {django_solomon-0.2.0.dist-info → django_solomon-0.4.0.dist-info}/WHEEL +0 -0
- {django_solomon-0.2.0.dist-info → django_solomon-0.4.0.dist-info}/licenses/LICENSE +0 -0
django_solomon/backends.py
CHANGED
@@ -6,6 +6,7 @@ from django.http import HttpRequest
|
|
6
6
|
from django.utils.translation import gettext_lazy as _
|
7
7
|
|
8
8
|
from django_solomon.models import MagicLink, UserType
|
9
|
+
from django_solomon.utilities import get_client_ip
|
9
10
|
|
10
11
|
|
11
12
|
class MagicLinkBackend(ModelBackend):
|
@@ -41,6 +42,21 @@ class MagicLinkBackend(ModelBackend):
|
|
41
42
|
request.magic_link_error = _("Invalid or expired magic link")
|
42
43
|
return None
|
43
44
|
|
45
|
+
# Check if IP validation is enabled
|
46
|
+
if getattr(settings, "SOLOMON_ENFORCE_SAME_IP", False):
|
47
|
+
# Only check IP if we have a request object
|
48
|
+
if request is not None:
|
49
|
+
# Get current IP address
|
50
|
+
current_ip = get_client_ip(request)
|
51
|
+
|
52
|
+
# Compare with stored IP address
|
53
|
+
if magic_link.ip_address and current_ip != magic_link.ip_address:
|
54
|
+
request.magic_link_error = _("IP address mismatch. Please request a new magic link.")
|
55
|
+
return None
|
56
|
+
else:
|
57
|
+
# If IP validation is enabled but we don't have a request object, we can't validate the IP
|
58
|
+
return None
|
59
|
+
|
44
60
|
# Mark the magic link as used
|
45
61
|
magic_link.use()
|
46
62
|
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# Generated by Django 5.2 on 2025-04-20 14:59
|
2
|
+
|
3
|
+
from django.db import migrations, models
|
4
|
+
|
5
|
+
|
6
|
+
class Migration(migrations.Migration):
|
7
|
+
|
8
|
+
dependencies = [
|
9
|
+
('django_solomon', '0001_initial'),
|
10
|
+
]
|
11
|
+
|
12
|
+
operations = [
|
13
|
+
migrations.AddField(
|
14
|
+
model_name='magiclink',
|
15
|
+
name='ip_address',
|
16
|
+
field=models.GenericIPAddressField(blank=True, null=True, verbose_name='IP Address'),
|
17
|
+
),
|
18
|
+
]
|
@@ -0,0 +1,43 @@
|
|
1
|
+
from django.db import migrations, models
|
2
|
+
from django.db.models.functions import Lower
|
3
|
+
|
4
|
+
|
5
|
+
# Copyright (c) 2023 Carlton Gibson
|
6
|
+
# This migration is taken from the fantastic project `django-unique-user-email` created by Carlton Gibson.
|
7
|
+
# https://github.com/carltongibson/django-unique-user-email/
|
8
|
+
|
9
|
+
|
10
|
+
class CustomAddConstraint(migrations.AddConstraint):
|
11
|
+
"""
|
12
|
+
Override app_label to target auth.User
|
13
|
+
"""
|
14
|
+
|
15
|
+
def state_forwards(self, app_label, state):
|
16
|
+
state.add_constraint("auth", self.model_name_lower, self.constraint)
|
17
|
+
|
18
|
+
def database_forwards(self, app_label, schema_editor, from_state, to_state):
|
19
|
+
model = to_state.apps.get_model("auth", self.model_name)
|
20
|
+
if self.allow_migrate_model(schema_editor.connection.alias, model):
|
21
|
+
schema_editor.add_constraint(model, self.constraint)
|
22
|
+
|
23
|
+
def database_backwards(self, app_label, schema_editor, from_state, to_state):
|
24
|
+
model = to_state.apps.get_model("auth", self.model_name)
|
25
|
+
if self.allow_migrate_model(schema_editor.connection.alias, model):
|
26
|
+
schema_editor.remove_constraint(model, self.constraint)
|
27
|
+
|
28
|
+
|
29
|
+
class Migration(migrations.Migration):
|
30
|
+
dependencies = [
|
31
|
+
("django_solomon", "0002_magiclink_ip_address"),
|
32
|
+
("auth", "0012_alter_user_first_name_max_length"),
|
33
|
+
]
|
34
|
+
|
35
|
+
operations = [
|
36
|
+
CustomAddConstraint(
|
37
|
+
model_name="user",
|
38
|
+
constraint=models.UniqueConstraint(
|
39
|
+
Lower("email"),
|
40
|
+
name="unique_user_email"
|
41
|
+
),
|
42
|
+
),
|
43
|
+
]
|
django_solomon/models.py
CHANGED
@@ -42,7 +42,7 @@ class MagicLinkManager(models.Manager["MagicLink"]):
|
|
42
42
|
criteria like token, usage status, and expiration date.
|
43
43
|
"""
|
44
44
|
|
45
|
-
def create_for_user(self, user: UserType) -> "MagicLink":
|
45
|
+
def create_for_user(self, user: UserType, ip_address: str = None) -> "MagicLink":
|
46
46
|
"""
|
47
47
|
Creates a new magic link for a given user. If the setting SOLOMON_ONLY_ONE_LINK_ALLOWED
|
48
48
|
is enabled, marks existing links for the user as used before creating a new one.
|
@@ -55,7 +55,7 @@ class MagicLinkManager(models.Manager["MagicLink"]):
|
|
55
55
|
"""
|
56
56
|
if getattr(settings, "SOLOMON_ONLY_ONE_LINK_ALLOWED", True):
|
57
57
|
self.filter(user=user).update(used=True)
|
58
|
-
return self.create(user=user)
|
58
|
+
return self.create(user=user, ip_address=ip_address)
|
59
59
|
|
60
60
|
def get_valid_link(self, token: str) -> "MagicLink":
|
61
61
|
"""
|
@@ -90,6 +90,7 @@ class MagicLink(models.Model):
|
|
90
90
|
created_at = models.DateTimeField(_("Created at"), auto_now_add=True)
|
91
91
|
expires_at = models.DateTimeField(_("Expires at"))
|
92
92
|
used = models.BooleanField(_("Used"), default=False)
|
93
|
+
ip_address = models.GenericIPAddressField(_("IP Address"), null=True, blank=True)
|
93
94
|
|
94
95
|
objects = MagicLinkManager()
|
95
96
|
|
django_solomon/utilities.py
CHANGED
@@ -1,5 +1,8 @@
|
|
1
|
+
import ipaddress
|
1
2
|
import logging
|
3
|
+
import socket
|
2
4
|
|
5
|
+
from django.conf import settings
|
3
6
|
from django.contrib.auth import get_user_model
|
4
7
|
from django.contrib.auth.hashers import make_password
|
5
8
|
|
@@ -79,3 +82,69 @@ def create_user_from_email(email: str) -> UserType:
|
|
79
82
|
user_kwargs[email_field] = email
|
80
83
|
|
81
84
|
return user_model.objects.create_user(**user_kwargs)
|
85
|
+
|
86
|
+
|
87
|
+
def get_ip_from_hostname(hostname: str) -> str | None:
|
88
|
+
try:
|
89
|
+
addr_info = socket.getaddrinfo(hostname, None)
|
90
|
+
if addr_info:
|
91
|
+
return addr_info[0][4][0]
|
92
|
+
return None
|
93
|
+
except socket.gaierror:
|
94
|
+
return None
|
95
|
+
|
96
|
+
|
97
|
+
def anonymize_ip(ip_str: str) -> str:
|
98
|
+
"""
|
99
|
+
Anonymize an IP address by removing the last octet for IPv4 or
|
100
|
+
the last 80 bits (last 5 segments) for IPv6.
|
101
|
+
|
102
|
+
Args:
|
103
|
+
ip_str: The IP address to anonymize.
|
104
|
+
|
105
|
+
Returns:
|
106
|
+
The anonymized IP address.
|
107
|
+
"""
|
108
|
+
try:
|
109
|
+
ip = ipaddress.ip_address(ip_str)
|
110
|
+
|
111
|
+
if isinstance(ip, ipaddress.IPv4Address):
|
112
|
+
# For IPv4, zero out the last octet
|
113
|
+
# Convert to integer, mask with 0xFFFFFF00 to zero last octet, convert back
|
114
|
+
masked_ip = ipaddress.IPv4Address(int(ip) & 0xFFFFFF00)
|
115
|
+
return str(masked_ip)
|
116
|
+
elif isinstance(ip, ipaddress.IPv6Address):
|
117
|
+
# For IPv6, zero out the last 80 bits (last 5 segments)
|
118
|
+
# Convert to integer, mask with ~(2^80-1) to zero last 80 bits, convert back
|
119
|
+
masked_ip = ipaddress.IPv6Address(int(ip) & ~((1 << 80) - 1))
|
120
|
+
return str(masked_ip)
|
121
|
+
return ip_str
|
122
|
+
except ValueError:
|
123
|
+
# If it's not a valid IP address, return as is
|
124
|
+
return ip_str
|
125
|
+
|
126
|
+
|
127
|
+
def get_client_ip(request) -> str:
|
128
|
+
# Check X-Forwarded-For header first
|
129
|
+
x_forwarded_for = request.headers.get("x-forwarded-for")
|
130
|
+
if x_forwarded_for:
|
131
|
+
ip = x_forwarded_for.split(",")[0].strip()
|
132
|
+
else:
|
133
|
+
ip = request.META.get("REMOTE_ADDR")
|
134
|
+
|
135
|
+
# Check if the value is a valid IP address
|
136
|
+
try:
|
137
|
+
ipaddress.ip_address(ip)
|
138
|
+
# Anonymize IP if the setting is enabled (default is True)
|
139
|
+
if getattr(settings, "SOLOMON_ANONYMIZE_IP", True):
|
140
|
+
return anonymize_ip(ip)
|
141
|
+
return ip
|
142
|
+
except ValueError:
|
143
|
+
# Not a valid IP, try to resolve as hostname
|
144
|
+
resolved_ip = get_ip_from_hostname(ip)
|
145
|
+
if resolved_ip:
|
146
|
+
# Anonymize the resolved IP if the setting is enabled
|
147
|
+
if getattr(settings, "SOLOMON_ANONYMIZE_IP", True):
|
148
|
+
return anonymize_ip(resolved_ip)
|
149
|
+
return resolved_ip
|
150
|
+
return ip
|
django_solomon/views.py
CHANGED
@@ -19,7 +19,7 @@ from django_solomon.config import (
|
|
19
19
|
)
|
20
20
|
from django_solomon.forms import MagicLinkForm
|
21
21
|
from django_solomon.models import BlacklistedEmail, MagicLink
|
22
|
-
from django_solomon.utilities import get_user_by_email, create_user_from_email
|
22
|
+
from django_solomon.utilities import get_user_by_email, create_user_from_email, get_client_ip
|
23
23
|
|
24
24
|
logger = logging.getLogger(__name__)
|
25
25
|
User = get_user_model()
|
@@ -58,7 +58,12 @@ def send_magic_link(request: HttpRequest) -> HttpResponse:
|
|
58
58
|
|
59
59
|
# Send magic link if user exists
|
60
60
|
if user:
|
61
|
-
|
61
|
+
# Get the user's IP address
|
62
|
+
ip_address = get_client_ip(request)
|
63
|
+
|
64
|
+
# Create magic link with IP address
|
65
|
+
magic_link = MagicLink.objects.create(user=user, ip_address=ip_address)
|
66
|
+
|
62
67
|
link_url = request.build_absolute_uri(
|
63
68
|
reverse(
|
64
69
|
"django_solomon:validate_magic_link",
|
@@ -1,10 +1,11 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: django-solomon
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.4.0
|
4
4
|
Summary: A Django app for passwordless authentication using magic links.
|
5
5
|
Project-URL: Home, https://django-solomon.rtfd.io/
|
6
6
|
Project-URL: Documentation, https://django-solomon.rtfd.io/
|
7
7
|
Project-URL: Repository, https://codeberg.org/oliverandrich/django-solomon
|
8
|
+
Project-URL: Issue Tracker, https://codeberg.org/oliverandrich/django-solomon/issues
|
8
9
|
Author-email: Oliver Andrich <oliver@andrich.me>
|
9
10
|
License: MIT License
|
10
11
|
|
@@ -52,11 +53,10 @@ Description-Content-Type: text/markdown
|
|
52
53
|
# django-solomon
|
53
54
|
|
54
55
|
[](https://pypi.org/project/django-solomon/)
|
56
|
+

|
55
57
|
[](https://pypi.org/project/django-solomon/)
|
56
58
|
[](https://pypi.org/project/django-solomon/)
|
57
59
|
[](https://django-solomon.rtfd.io/en/latest/?badge=latest)
|
58
|
-
[](https://pepy.tech/project/django-solomon)
|
59
|
-
[](https://pepy.tech/project/django-solomon)
|
60
60
|
[](https://github.com/astral-sh/ruff)
|
61
61
|
[](https://github.com/astral-sh/uv)
|
62
62
|
|
@@ -68,8 +68,11 @@ A Django app for passwordless authentication using magic links.
|
|
68
68
|
- Configurable link expiration time
|
69
69
|
- Blacklist functionality to block specific email addresses
|
70
70
|
- Support for auto-creating users when they request a magic link
|
71
|
+
- IP address tracking and validation for enhanced security
|
72
|
+
- Privacy-focused IP anonymization
|
71
73
|
- Customizable templates for emails and pages
|
72
74
|
- Compatible with Django's authentication system
|
75
|
+
- Enforces unique email addresses for users (case-insensitive)
|
73
76
|
|
74
77
|
## Installation
|
75
78
|
|
@@ -134,19 +137,21 @@ DEFAULT_FROM_EMAIL = 'your-email@example.com'
|
|
134
137
|
|
135
138
|
django-solomon provides several settings that you can customize in your Django settings file:
|
136
139
|
|
137
|
-
| Setting | Default | Description
|
138
|
-
|
139
|
-
| `SOLOMON_LINK_EXPIRATION` | `300` | The expiration time for magic links in seconds
|
140
|
-
| `SOLOMON_ONLY_ONE_LINK_ALLOWED` | `True` | If enabled, only one active magic link is allowed per user
|
141
|
-
| `SOLOMON_CREATE_USER_IF_NOT_FOUND` | `False` | If enabled, creates a new user when a magic link is requested for a non-existent email
|
142
|
-
| `SOLOMON_LOGIN_REDIRECT_URL` | `settings.LOGIN_REDIRECT_URL` | The URL to redirect to after successful authentication
|
143
|
-
| `SOLOMON_ALLOW_ADMIN_LOGIN` | `True` | If enabled, allows superusers to log in using magic links
|
144
|
-
| `SOLOMON_ALLOW_STAFF_LOGIN` | `True` | If enabled, allows staff users to log in using magic links
|
145
|
-
| `SOLOMON_MAIL_TEXT_TEMPLATE` | `"django_solomon/email/magic_link.txt"` | The template to use for plain text magic link emails
|
146
|
-
| `SOLOMON_MAIL_MJML_TEMPLATE` | `"django_solomon/email/magic_link.mjml"` | The template to use for HTML magic link emails (MJML format)
|
147
|
-
| `SOLOMON_LOGIN_FORM_TEMPLATE` | `"django_solomon/base/login_form.html"` | The template to use for the login form page
|
148
|
-
| `SOLOMON_INVALID_MAGIC_LINK_TEMPLATE` | `"django_solomon/base/invalid_magic_link.html"` | The template to use for the invalid magic link page
|
149
|
-
| `SOLOMON_MAGIC_LINK_SENT_TEMPLATE` | `"django_solomon/base/magic_link_sent.html"` | The template to use for the magic link sent confirmation page
|
140
|
+
| Setting | Default | Description |
|
141
|
+
|---------------------------------------|-------------------------------------------------|-----------------------------------------------------------------------------------------|
|
142
|
+
| `SOLOMON_LINK_EXPIRATION` | `300` | The expiration time for magic links in seconds |
|
143
|
+
| `SOLOMON_ONLY_ONE_LINK_ALLOWED` | `True` | If enabled, only one active magic link is allowed per user |
|
144
|
+
| `SOLOMON_CREATE_USER_IF_NOT_FOUND` | `False` | If enabled, creates a new user when a magic link is requested for a non-existent email |
|
145
|
+
| `SOLOMON_LOGIN_REDIRECT_URL` | `settings.LOGIN_REDIRECT_URL` | The URL to redirect to after successful authentication |
|
146
|
+
| `SOLOMON_ALLOW_ADMIN_LOGIN` | `True` | If enabled, allows superusers to log in using magic links |
|
147
|
+
| `SOLOMON_ALLOW_STAFF_LOGIN` | `True` | If enabled, allows staff users to log in using magic links |
|
148
|
+
| `SOLOMON_MAIL_TEXT_TEMPLATE` | `"django_solomon/email/magic_link.txt"` | The template to use for plain text magic link emails |
|
149
|
+
| `SOLOMON_MAIL_MJML_TEMPLATE` | `"django_solomon/email/magic_link.mjml"` | The template to use for HTML magic link emails (MJML format) |
|
150
|
+
| `SOLOMON_LOGIN_FORM_TEMPLATE` | `"django_solomon/base/login_form.html"` | The template to use for the login form page |
|
151
|
+
| `SOLOMON_INVALID_MAGIC_LINK_TEMPLATE` | `"django_solomon/base/invalid_magic_link.html"` | The template to use for the invalid magic link page |
|
152
|
+
| `SOLOMON_MAGIC_LINK_SENT_TEMPLATE` | `"django_solomon/base/magic_link_sent.html"` | The template to use for the magic link sent confirmation page |
|
153
|
+
| `SOLOMON_ENFORCE_SAME_IP` | `False` | If enabled, validates that magic links are used from the same IP they were created from |
|
154
|
+
| `SOLOMON_ANONYMIZE_IP` | `True` | If enabled, anonymizes IP addresses before storing them (removes last octet for IPv4) |
|
150
155
|
|
151
156
|
## Usage
|
152
157
|
|
@@ -183,10 +188,15 @@ You can also use django-solomon programmatically in your views:
|
|
183
188
|
```python
|
184
189
|
from django.contrib.auth import authenticate, login
|
185
190
|
from django_solomon.models import MagicLink
|
191
|
+
from django_solomon.utilities import get_client_ip
|
186
192
|
|
187
193
|
# Create a magic link for a user
|
188
194
|
magic_link = MagicLink.objects.create_for_user(user)
|
189
195
|
|
196
|
+
# Create a magic link with IP address tracking
|
197
|
+
ip_address = get_client_ip(request)
|
198
|
+
magic_link = MagicLink.objects.create_for_user(user, ip_address=ip_address)
|
199
|
+
|
190
200
|
# Authenticate a user with a token
|
191
201
|
user = authenticate(request=request, token=token)
|
192
202
|
if user:
|
@@ -195,11 +205,13 @@ if user:
|
|
195
205
|
|
196
206
|
## Documentation
|
197
207
|
|
198
|
-
For more detailed information, tutorials, and advanced usage examples, please visit
|
208
|
+
For more detailed information, tutorials, and advanced usage examples, please visit
|
209
|
+
the [official documentation](https://django-solomon.rtfd.io/).
|
199
210
|
|
200
211
|
## License
|
201
212
|
|
202
|
-
This software is licensed
|
213
|
+
This software is licensed
|
214
|
+
under [MIT license](https://codeberg.org/oliverandrich/django-solomon/src/branch/main/LICENSE).
|
203
215
|
|
204
216
|
## Contributing
|
205
217
|
|
@@ -1,26 +1,28 @@
|
|
1
1
|
django_solomon/__init__.py,sha256=4GdjeQVyChzdc7pZ1jrpknjcnu9Do_0nSh42UZNICKo,80
|
2
2
|
django_solomon/admin.py,sha256=hwesCOQgUDDHPh5qq97AObfSyMgIOKsdzoa69naMPx4,1428
|
3
3
|
django_solomon/apps.py,sha256=glfjdSSzrxuwJ2FrzuF2C4oFNqikMqhSHHmca2qq5Vg,159
|
4
|
-
django_solomon/backends.py,sha256=
|
4
|
+
django_solomon/backends.py,sha256=3RN4dsCehg5urp9sfASERFda1VR9nislNvHrxUc7rXM,3171
|
5
5
|
django_solomon/config.py,sha256=JEl3cY0BnfC9ja0ZVeLmfIwUStbUAO6vRhGZrrig5Yw,787
|
6
6
|
django_solomon/forms.py,sha256=ymDsZ-KWyJfo12vGbZCxIhIvVket93xYR4MgDif4fh0,312
|
7
|
-
django_solomon/models.py,sha256=
|
7
|
+
django_solomon/models.py,sha256=G-74LDemMJMVOXN94tSz7E9XwspCNkzmHbhEP3Kl80U,4904
|
8
8
|
django_solomon/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
9
9
|
django_solomon/urls.py,sha256=tT-I-p6dA2rD1IvH3pAcJRkPXYW0oYmYWXZAoiLJzGY,471
|
10
|
-
django_solomon/utilities.py,sha256=
|
11
|
-
django_solomon/views.py,sha256=
|
10
|
+
django_solomon/utilities.py,sha256=I6LiBwAwZaa8h-iH_6RLP9fkJkK9mEb_24LPzLLuwFA,4559
|
11
|
+
django_solomon/views.py,sha256=gqsj1SVd2bbQvla3xpZYVHYCUxhu3fr_5I-LY-Tvm2g,5737
|
12
12
|
django_solomon/locale/de/LC_MESSAGES/django.po,sha256=6lo72lsDV1bKBVowiRsQoaVKH5Mbkf34z78sG1LEYq4,747
|
13
13
|
django_solomon/locale/en/LC_MESSAGES/django.po,sha256=6MjGCWQn-1AjOnP_gXtLa0Oad4qfNAn5tXdhnw_wEcg,732
|
14
14
|
django_solomon/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
15
15
|
django_solomon/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
16
16
|
django_solomon/migrations/0001_initial.py,sha256=i-UhR1KQ4p7WmbDg9xUQfMmDo8yYdIigvHNsefLHJEc,1981
|
17
|
+
django_solomon/migrations/0002_magiclink_ip_address.py,sha256=hs3MISYV7IHEqF-vELCmFtzhhvK8GEHkZu8RMc4X1YU,432
|
18
|
+
django_solomon/migrations/0003_add_unique_constraint_auth_user_email.py,sha256=9-etXYGGm72G5bmeSdQNpxfQsVl2fTPLyt2sP-CK0G0,1529
|
17
19
|
django_solomon/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
18
20
|
django_solomon/templates/django_solomon/base/invalid_magic_link.html,sha256=3U4T2n4uvPvktiOOHYVyNmk0aXFfE4DPrURE1DinWKk,554
|
19
21
|
django_solomon/templates/django_solomon/base/login_form.html,sha256=VYGnrno3c36W9f04XdRqJpjsDfOy0sq5D_cLayPz8Q0,546
|
20
22
|
django_solomon/templates/django_solomon/base/magic_link_sent.html,sha256=GIMxnw3E98TXVkVQkMRTHmX5mz0xUsvgXVj25gO2HPQ,461
|
21
23
|
django_solomon/templates/django_solomon/email/magic_link.mjml,sha256=OnfVGm2yFrOmoQ1syo6-Duq_Qraf3Tv0Vy5oidvt55g,1427
|
22
24
|
django_solomon/templates/django_solomon/email/magic_link.txt,sha256=yl01eie3U2HkFEJvB1Qlm8Z6FPuop5yubDXFY5V507Q,502
|
23
|
-
django_solomon-0.
|
24
|
-
django_solomon-0.
|
25
|
-
django_solomon-0.
|
26
|
-
django_solomon-0.
|
25
|
+
django_solomon-0.4.0.dist-info/METADATA,sha256=Hi9StTi9V1wK7U1HEeYG1PJiycawWMkrbzQHCns8EX0,10409
|
26
|
+
django_solomon-0.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
27
|
+
django_solomon-0.4.0.dist-info/licenses/LICENSE,sha256=tbfFOFdH5mHIfmNGo6KGOC3U8f-hTCLfetcr8A89WwI,1071
|
28
|
+
django_solomon-0.4.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|