django-solomon 0.1.2__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/__init__.py +3 -0
- django_solomon/admin.py +45 -0
- django_solomon/apps.py +6 -0
- django_solomon/backends.py +61 -0
- django_solomon/config.py +23 -0
- django_solomon/forms.py +12 -0
- django_solomon/locale/de/LC_MESSAGES/django.po +27 -0
- django_solomon/locale/en/LC_MESSAGES/django.po +27 -0
- django_solomon/management/__init__.py +0 -0
- django_solomon/management/commands/__init__.py +0 -0
- django_solomon/migrations/0001_initial.py +47 -0
- django_solomon/migrations/__init__.py +0 -0
- django_solomon/models.py +133 -0
- django_solomon/py.typed +0 -0
- django_solomon/templates/django_solomon/base/invalid_magic_link.html +16 -0
- django_solomon/templates/django_solomon/base/login_form.html +17 -0
- django_solomon/templates/django_solomon/base/magic_link_sent.html +12 -0
- django_solomon/templates/django_solomon/email/magic_link.mjml +42 -0
- django_solomon/templates/django_solomon/email/magic_link.txt +14 -0
- django_solomon/urls.py +23 -0
- django_solomon/utilities.py +81 -0
- django_solomon/views.py +141 -0
- django_solomon-0.1.2.dist-info/METADATA +198 -0
- django_solomon-0.1.2.dist-info/RECORD +26 -0
- django_solomon-0.1.2.dist-info/WHEEL +4 -0
- django_solomon-0.1.2.dist-info/licenses/LICENSE +21 -0
django_solomon/admin.py
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
from django.contrib import admin
|
2
|
+
from django.utils.translation import gettext_lazy as _
|
3
|
+
|
4
|
+
from django_solomon.models import BlacklistedEmail, MagicLink
|
5
|
+
|
6
|
+
|
7
|
+
@admin.register(BlacklistedEmail)
|
8
|
+
class BlacklistedEmailAdmin(admin.ModelAdmin):
|
9
|
+
"""Admin interface for BlacklistedEmail model."""
|
10
|
+
|
11
|
+
list_display = ("email", "reason", "created_at")
|
12
|
+
list_filter = ("created_at",)
|
13
|
+
search_fields = ("email", "reason")
|
14
|
+
date_hierarchy = "created_at"
|
15
|
+
|
16
|
+
fieldsets = (
|
17
|
+
(None, {"fields": ("email", "reason")}),
|
18
|
+
(_("Dates"), {"fields": ("created_at",)}),
|
19
|
+
)
|
20
|
+
readonly_fields = ("created_at",)
|
21
|
+
|
22
|
+
|
23
|
+
@admin.register(MagicLink)
|
24
|
+
class MagicLinkAdmin(admin.ModelAdmin):
|
25
|
+
"""Admin interface for MagicLink model."""
|
26
|
+
|
27
|
+
list_display = ("user", "created_at", "expires_at", "used", "is_valid")
|
28
|
+
list_filter = ("used", "created_at", "expires_at")
|
29
|
+
search_fields = ("user__username", "user__email", "token")
|
30
|
+
readonly_fields = ("id", "token", "created_at", "is_valid")
|
31
|
+
date_hierarchy = "created_at"
|
32
|
+
|
33
|
+
fieldsets = (
|
34
|
+
(None, {"fields": ("id", "user", "token")}),
|
35
|
+
(_("Status"), {"fields": ("used", "is_valid")}),
|
36
|
+
(_("Dates"), {"fields": ("created_at", "expires_at")}),
|
37
|
+
)
|
38
|
+
|
39
|
+
@admin.display(
|
40
|
+
description=_("Valid"),
|
41
|
+
boolean=True,
|
42
|
+
)
|
43
|
+
def is_valid(self, obj: MagicLink) -> bool:
|
44
|
+
"""Display if the magic link is valid."""
|
45
|
+
return obj.is_valid
|
django_solomon/apps.py
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
3
|
+
from django.conf import settings
|
4
|
+
from django.contrib.auth.backends import ModelBackend
|
5
|
+
from django.http import HttpRequest
|
6
|
+
from django.utils.translation import gettext_lazy as _
|
7
|
+
|
8
|
+
from django_solomon.models import MagicLink, UserType
|
9
|
+
|
10
|
+
|
11
|
+
class MagicLinkBackend(ModelBackend):
|
12
|
+
def authenticate(
|
13
|
+
self,
|
14
|
+
request: HttpRequest | None = None,
|
15
|
+
token: str | None = None,
|
16
|
+
**kwargs: Any, # noqa: ARG002
|
17
|
+
) -> UserType | None:
|
18
|
+
"""
|
19
|
+
Authenticates a user based on the provided magic link token. This method validates
|
20
|
+
the token, ensures the token is not expired, and checks if the user associated
|
21
|
+
with the token is allowed to login based on their role and the application's
|
22
|
+
settings.
|
23
|
+
|
24
|
+
Args:
|
25
|
+
request (HttpRequest | None): The HTTP request object. Defaults to None.
|
26
|
+
It is used to store error messages in case the authentication fails.
|
27
|
+
token (str | None): A unique string token from the magic link used for
|
28
|
+
authentication. If None, authentication cannot proceed.
|
29
|
+
|
30
|
+
Returns:
|
31
|
+
Any: Returns the authenticated user object if the token is valid and
|
32
|
+
the user is allowed to log in. Returns None if the token is invalid,
|
33
|
+
expired, or if the user is not permitted to log in.
|
34
|
+
"""
|
35
|
+
if not token:
|
36
|
+
return None
|
37
|
+
|
38
|
+
magic_link = MagicLink.objects.get_valid_link(token)
|
39
|
+
if not magic_link:
|
40
|
+
if request:
|
41
|
+
request.magic_link_error = _("Invalid or expired magic link")
|
42
|
+
return None
|
43
|
+
|
44
|
+
# Mark the magic link as used
|
45
|
+
magic_link.use()
|
46
|
+
|
47
|
+
user = magic_link.user
|
48
|
+
|
49
|
+
# Check if user is admin and if they are allowed to login
|
50
|
+
if user.is_superuser and not getattr(settings, "SOLOMON_ALLOW_ADMIN_LOGIN", True):
|
51
|
+
if request:
|
52
|
+
request.magic_link_error = _("Admin users are not allowed to login via magic links")
|
53
|
+
return None
|
54
|
+
|
55
|
+
# Check if user is staff (but not admin) and if they are allowed to login
|
56
|
+
if user.is_staff and not user.is_superuser and not getattr(settings, "SOLOMON_ALLOW_STAFF_LOGIN", True):
|
57
|
+
if request:
|
58
|
+
request.magic_link_error = _("Staff users are not allowed to login via magic links")
|
59
|
+
return None
|
60
|
+
|
61
|
+
return user
|
django_solomon/config.py
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
from django.conf import settings
|
2
|
+
|
3
|
+
|
4
|
+
def get_email_text_template():
|
5
|
+
return getattr(settings, "SOLOMON_MAIL_TEXT_TEMPLATE", None) or "django_solomon/email/magic_link.txt"
|
6
|
+
|
7
|
+
|
8
|
+
def get_email_mjml_template():
|
9
|
+
return getattr(settings, "SOLOMON_MAIL_MJML_TEMPLATE", None) or "django_solomon/email/magic_link.mjml"
|
10
|
+
|
11
|
+
|
12
|
+
def get_login_form_template():
|
13
|
+
return getattr(settings, "SOLOMON_LOGIN_FORM_TEMPLATE", None) or "django_solomon/base/login_form.html"
|
14
|
+
|
15
|
+
|
16
|
+
def get_invalid_magic_link_template():
|
17
|
+
return (
|
18
|
+
getattr(settings, "SOLOMON_INVALID_MAGIC_LINK_TEMPLATE", None) or "django_solomon/base/invalid_magic_link.html"
|
19
|
+
)
|
20
|
+
|
21
|
+
|
22
|
+
def get_magic_link_sent_template():
|
23
|
+
return getattr(settings, "SOLOMON_MAGIC_LINK_SENT_TEMPLATE", None) or "django_solomon/base/magic_link_sent.html"
|
django_solomon/forms.py
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
from django import forms
|
2
|
+
from django.utils.translation import gettext_lazy as _
|
3
|
+
|
4
|
+
|
5
|
+
class MagicLinkForm(forms.Form):
|
6
|
+
"""Form for requesting a magic link."""
|
7
|
+
|
8
|
+
email = forms.EmailField(
|
9
|
+
label=_("Email"),
|
10
|
+
max_length=254,
|
11
|
+
widget=forms.EmailInput(attrs={"autocomplete": "email"}),
|
12
|
+
)
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# SOME DESCRIPTIVE TITLE.
|
2
|
+
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
3
|
+
# This file is distributed under the same license as the PACKAGE package.
|
4
|
+
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
5
|
+
#
|
6
|
+
#, fuzzy
|
7
|
+
msgid ""
|
8
|
+
msgstr ""
|
9
|
+
"Project-Id-Version: PACKAGE VERSION\n"
|
10
|
+
"Report-Msgid-Bugs-To: \n"
|
11
|
+
"POT-Creation-Date: 2023-03-04 23:44+0100\n"
|
12
|
+
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
13
|
+
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
14
|
+
"Language-Team: LANGUAGE <LL@li.org>\n"
|
15
|
+
"Language: \n"
|
16
|
+
"MIME-Version: 1.0\n"
|
17
|
+
"Content-Type: text/plain; charset=UTF-8\n"
|
18
|
+
"Content-Transfer-Encoding: 8bit\n"
|
19
|
+
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
20
|
+
|
21
|
+
#: config/settings.py:150
|
22
|
+
msgid "English"
|
23
|
+
msgstr "Englisch"
|
24
|
+
|
25
|
+
#: config/settings.py:151
|
26
|
+
msgid "German"
|
27
|
+
msgstr "Deutsch"
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# SOME DESCRIPTIVE TITLE.
|
2
|
+
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
3
|
+
# This file is distributed under the same license as the PACKAGE package.
|
4
|
+
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
5
|
+
#
|
6
|
+
#, fuzzy
|
7
|
+
msgid ""
|
8
|
+
msgstr ""
|
9
|
+
"Project-Id-Version: PACKAGE VERSION\n"
|
10
|
+
"Report-Msgid-Bugs-To: \n"
|
11
|
+
"POT-Creation-Date: 2023-03-04 23:44+0100\n"
|
12
|
+
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
13
|
+
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
14
|
+
"Language-Team: LANGUAGE <LL@li.org>\n"
|
15
|
+
"Language: \n"
|
16
|
+
"MIME-Version: 1.0\n"
|
17
|
+
"Content-Type: text/plain; charset=UTF-8\n"
|
18
|
+
"Content-Transfer-Encoding: 8bit\n"
|
19
|
+
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
20
|
+
|
21
|
+
#: config/settings.py:150
|
22
|
+
msgid "English"
|
23
|
+
msgstr ""
|
24
|
+
|
25
|
+
#: config/settings.py:151
|
26
|
+
msgid "German"
|
27
|
+
msgstr ""
|
File without changes
|
File without changes
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# Generated by Django 5.2 on 2025-04-18 13:46
|
2
|
+
|
3
|
+
import django.db.models.deletion
|
4
|
+
from django.conf import settings
|
5
|
+
from django.db import migrations, models
|
6
|
+
|
7
|
+
|
8
|
+
class Migration(migrations.Migration):
|
9
|
+
|
10
|
+
initial = True
|
11
|
+
|
12
|
+
dependencies = [
|
13
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
14
|
+
]
|
15
|
+
|
16
|
+
operations = [
|
17
|
+
migrations.CreateModel(
|
18
|
+
name='BlacklistedEmail',
|
19
|
+
fields=[
|
20
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
21
|
+
('email', models.EmailField(max_length=255, unique=True, verbose_name='Email')),
|
22
|
+
('reason', models.TextField(blank=True, verbose_name='Reason')),
|
23
|
+
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
24
|
+
],
|
25
|
+
options={
|
26
|
+
'verbose_name': 'Blacklisted Email',
|
27
|
+
'verbose_name_plural': 'Blacklisted Emails',
|
28
|
+
'ordering': ['-created_at'],
|
29
|
+
},
|
30
|
+
),
|
31
|
+
migrations.CreateModel(
|
32
|
+
name='MagicLink',
|
33
|
+
fields=[
|
34
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
35
|
+
('token', models.CharField(max_length=100, unique=True, verbose_name='Token')),
|
36
|
+
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
37
|
+
('expires_at', models.DateTimeField(verbose_name='Expires at')),
|
38
|
+
('used', models.BooleanField(default=False, verbose_name='Used')),
|
39
|
+
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='magic_links', to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
40
|
+
],
|
41
|
+
options={
|
42
|
+
'verbose_name': 'Magic Link',
|
43
|
+
'verbose_name_plural': 'Magic Links',
|
44
|
+
'ordering': ['-created_at'],
|
45
|
+
},
|
46
|
+
),
|
47
|
+
]
|
File without changes
|
django_solomon/models.py
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
from datetime import timedelta
|
2
|
+
from typing import Any, TypeVar
|
3
|
+
|
4
|
+
from django.conf import settings
|
5
|
+
from django.contrib.auth import get_user_model
|
6
|
+
from django.contrib.auth.base_user import AbstractBaseUser
|
7
|
+
from django.contrib.auth.tokens import default_token_generator
|
8
|
+
from django.db import models
|
9
|
+
from django.utils import timezone
|
10
|
+
from django.utils.translation import gettext_lazy as _
|
11
|
+
|
12
|
+
User = get_user_model()
|
13
|
+
UserType = TypeVar("UserType", bound=AbstractBaseUser)
|
14
|
+
|
15
|
+
|
16
|
+
class BlacklistedEmail(models.Model):
|
17
|
+
"""
|
18
|
+
Model to store blacklisted email addresses.
|
19
|
+
|
20
|
+
These emails will be blocked from using the magic login feature.
|
21
|
+
"""
|
22
|
+
|
23
|
+
email = models.EmailField(_("Email"), max_length=255, unique=True)
|
24
|
+
reason = models.TextField(_("Reason"), blank=True)
|
25
|
+
created_at = models.DateTimeField(_("Created at"), auto_now_add=True)
|
26
|
+
|
27
|
+
class Meta:
|
28
|
+
verbose_name = _("Blacklisted Email")
|
29
|
+
verbose_name_plural = _("Blacklisted Emails")
|
30
|
+
ordering = ["-created_at"]
|
31
|
+
|
32
|
+
def __str__(self) -> str:
|
33
|
+
return self.email
|
34
|
+
|
35
|
+
|
36
|
+
class MagicLinkManager(models.Manager["MagicLink"]):
|
37
|
+
"""
|
38
|
+
Manages the creation and retrieval of MagicLink instances.
|
39
|
+
|
40
|
+
This manager provides utility methods to handle magic links, such as creating a
|
41
|
+
new magic link for a user and retrieving valid magic links based on specific
|
42
|
+
criteria like token, usage status, and expiration date.
|
43
|
+
"""
|
44
|
+
|
45
|
+
def create_for_user(self, user: UserType) -> "MagicLink":
|
46
|
+
"""
|
47
|
+
Creates a new magic link for a given user. If the setting SOLOMON_ONLY_ONE_LINK_ALLOWED
|
48
|
+
is enabled, marks existing links for the user as used before creating a new one.
|
49
|
+
|
50
|
+
Args:
|
51
|
+
user (UserType): The user for whom the magic link should be created.
|
52
|
+
|
53
|
+
Returns:
|
54
|
+
MagicLink: The newly created magic link instance.
|
55
|
+
"""
|
56
|
+
if getattr(settings, "SOLOMON_ONLY_ONE_LINK_ALLOWED", True):
|
57
|
+
self.filter(user=user).update(used=True)
|
58
|
+
return self.create(user=user)
|
59
|
+
|
60
|
+
def get_valid_link(self, token: str) -> "MagicLink":
|
61
|
+
"""
|
62
|
+
Returns the first valid magic link that matches the given token.
|
63
|
+
|
64
|
+
A valid magic link is identified by a unique token, marked as unused, and
|
65
|
+
has an expiration date greater than or equal to the current time. The
|
66
|
+
method searches for a link meeting these conditions and returns the first
|
67
|
+
result if found.
|
68
|
+
|
69
|
+
Args:
|
70
|
+
token (str): The unique token used to identify the magic link.
|
71
|
+
|
72
|
+
Returns:
|
73
|
+
MagicLink: An instance of the valid MagicLink, or None if no valid link
|
74
|
+
is found.
|
75
|
+
"""
|
76
|
+
return self.filter(token=token, used=False, expires_at__gte=timezone.now()).first()
|
77
|
+
|
78
|
+
|
79
|
+
class MagicLink(models.Model):
|
80
|
+
"""
|
81
|
+
Model to store magic links for authentication.
|
82
|
+
|
83
|
+
This model supports both the standard User model and custom User models.
|
84
|
+
"""
|
85
|
+
|
86
|
+
user = models.ForeignKey(
|
87
|
+
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="magic_links", verbose_name=_("User")
|
88
|
+
)
|
89
|
+
token = models.CharField(_("Token"), max_length=100, unique=True)
|
90
|
+
created_at = models.DateTimeField(_("Created at"), auto_now_add=True)
|
91
|
+
expires_at = models.DateTimeField(_("Expires at"))
|
92
|
+
used = models.BooleanField(_("Used"), default=False)
|
93
|
+
|
94
|
+
objects = MagicLinkManager()
|
95
|
+
|
96
|
+
class Meta:
|
97
|
+
verbose_name = _("Magic Link")
|
98
|
+
verbose_name_plural = _("Magic Links")
|
99
|
+
ordering = ["-created_at"]
|
100
|
+
|
101
|
+
def __str__(self) -> str:
|
102
|
+
return f"Magic Link for {self.user}"
|
103
|
+
|
104
|
+
def save(self, *args: Any, **kwargs: Any) -> None:
|
105
|
+
"""
|
106
|
+
Saves the instance of the object, ensuring that a token and expiration
|
107
|
+
time are generated and set if not already provided. The token is created
|
108
|
+
using a default token generator, and the expiration time is calculated
|
109
|
+
based on a predefined duration from the settings.
|
110
|
+
"""
|
111
|
+
if not self.pk or not self.token:
|
112
|
+
self.token = default_token_generator.make_token(self.user)
|
113
|
+
|
114
|
+
if not self.expires_at:
|
115
|
+
expiration_time = getattr(settings, "SOLOMON_LINK_EXPIRATION", 300) # seconds
|
116
|
+
self.expires_at = timezone.now() + timedelta(seconds=expiration_time)
|
117
|
+
|
118
|
+
super().save(*args, **kwargs)
|
119
|
+
|
120
|
+
@property
|
121
|
+
def is_expired(self) -> bool:
|
122
|
+
"""Check if the magic link has expired."""
|
123
|
+
return self.expires_at < timezone.now()
|
124
|
+
|
125
|
+
@property
|
126
|
+
def is_valid(self) -> bool:
|
127
|
+
"""Check if the magic link is valid (not used and not expired)."""
|
128
|
+
return not self.used and not self.is_expired
|
129
|
+
|
130
|
+
def use(self) -> None:
|
131
|
+
"""Mark the magic link as used."""
|
132
|
+
self.used = True
|
133
|
+
self.save(update_fields=["used"])
|
django_solomon/py.typed
ADDED
File without changes
|
@@ -0,0 +1,16 @@
|
|
1
|
+
{% extends "base.html" %}
|
2
|
+
{% load i18n %}
|
3
|
+
|
4
|
+
{% block title %}{% translate "Invalid Magic Link" %}{% endblock %}
|
5
|
+
|
6
|
+
{% block content %}
|
7
|
+
<div class="container">
|
8
|
+
<h1>{% translate "Invalid Magic Link" %}</h1>
|
9
|
+
<p>{% translate "The magic link you clicked is invalid or has expired." %}</p>
|
10
|
+
<p>{% translate "Please request a new magic link to log in." %}</p>
|
11
|
+
|
12
|
+
<a href="{% url 'django_solomon:login' %}" class="btn btn-primary">
|
13
|
+
{% translate "Request New Magic Link" %}
|
14
|
+
</a>
|
15
|
+
</div>
|
16
|
+
{% endblock content %}
|
@@ -0,0 +1,17 @@
|
|
1
|
+
{% extends "base.html" %}
|
2
|
+
{% load i18n %}
|
3
|
+
|
4
|
+
{% block title %}{% translate "Magic Link Login" %}{% endblock %}
|
5
|
+
|
6
|
+
{% block content %}
|
7
|
+
<div class="container">
|
8
|
+
<h1>{% translate "Magic Link Login" %}</h1>
|
9
|
+
<p>{% translate "Enter your email address to receive a magic link for logging in." %}</p>
|
10
|
+
|
11
|
+
<form method="post">
|
12
|
+
{% csrf_token %}
|
13
|
+
{{ form.as_p }}
|
14
|
+
<button type="submit" class="btn btn-primary">{% translate "Send Magic Link" %}</button>
|
15
|
+
</form>
|
16
|
+
</div>
|
17
|
+
{% endblock content %}
|
@@ -0,0 +1,12 @@
|
|
1
|
+
{% extends "base.html" %}
|
2
|
+
{% load i18n %}
|
3
|
+
|
4
|
+
{% block title %}{% translate "Magic Link Sent" %}{% endblock %}
|
5
|
+
|
6
|
+
{% block content %}
|
7
|
+
<div class="container">
|
8
|
+
<h1>{% translate "Magic Link Sent" %}</h1>
|
9
|
+
<p>{% translate "If an account exists with the email address you provided, we've sent a magic link to that address." %}</p>
|
10
|
+
<p>{% translate "Please check your email and click the link to log in." %}</p>
|
11
|
+
</div>
|
12
|
+
{% endblock content %}
|
@@ -0,0 +1,42 @@
|
|
1
|
+
{% load i18n %}
|
2
|
+
<mjml>
|
3
|
+
<mj-body>
|
4
|
+
<mj-section>
|
5
|
+
<mj-column>
|
6
|
+
<mj-text>
|
7
|
+
<p>{% translate "Hello," %}</p>
|
8
|
+
|
9
|
+
<p>
|
10
|
+
{% blocktranslate %}
|
11
|
+
You requested a magic link to log in to your account. Please click the link below
|
12
|
+
to log in:
|
13
|
+
{% endblocktranslate %}
|
14
|
+
</p>
|
15
|
+
</mj-text>
|
16
|
+
|
17
|
+
<mj-button href="{{ magic_link_url }}">{% translate "Click here to log in" %}</mj-button>
|
18
|
+
|
19
|
+
<mj-text>
|
20
|
+
<p>
|
21
|
+
{% blocktranslate with expiry_time=expiry_time %}
|
22
|
+
This link will expire in {{ expiry_time }} minutes. If you did not
|
23
|
+
request this link, you can safely ignore this email.
|
24
|
+
{% endblocktranslate %}
|
25
|
+
</p>
|
26
|
+
|
27
|
+
<p>
|
28
|
+
{% translate "Thank you," %}
|
29
|
+
<br>
|
30
|
+
{% translate "The Django Solomon Team" %}
|
31
|
+
</p>
|
32
|
+
|
33
|
+
<p>
|
34
|
+
---
|
35
|
+
<br>
|
36
|
+
{% translate "This email was sent by Django Solomon." %}
|
37
|
+
</p>
|
38
|
+
</mj-text>
|
39
|
+
</mj-column>
|
40
|
+
</mj-section>
|
41
|
+
</mj-body>
|
42
|
+
</mjml>
|
@@ -0,0 +1,14 @@
|
|
1
|
+
{% load i18n %}
|
2
|
+
{% translate "Hello," %}
|
3
|
+
|
4
|
+
{% translate "You requested a magic link to log in to your account. Please click the link below to log in:" %}
|
5
|
+
|
6
|
+
{{ magic_link_url }}
|
7
|
+
|
8
|
+
{% blocktranslate with expiry_time=expiry_time %}This link will expire in {{ expiry_time }} minutes. If you did not request this link, you can safely ignore this email.{% endblocktranslate %}
|
9
|
+
|
10
|
+
{% translate "Thank you," %}
|
11
|
+
{% translate "The Django Solomon Team" %}
|
12
|
+
|
13
|
+
---
|
14
|
+
{% translate "This email was sent by Django Solomon." %}
|
django_solomon/urls.py
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
from django.urls import path
|
2
|
+
|
3
|
+
from django_solomon.views import validate_magic_link, magic_link_sent, send_magic_link
|
4
|
+
|
5
|
+
app_name = "django_solomon"
|
6
|
+
|
7
|
+
urlpatterns = [
|
8
|
+
path(
|
9
|
+
"magic-link/",
|
10
|
+
send_magic_link,
|
11
|
+
name="login",
|
12
|
+
),
|
13
|
+
path(
|
14
|
+
"magic-link/sent/",
|
15
|
+
magic_link_sent,
|
16
|
+
name="magic_link_sent",
|
17
|
+
),
|
18
|
+
path(
|
19
|
+
"magic-link/<str:token>/",
|
20
|
+
validate_magic_link,
|
21
|
+
name="validate_magic_link",
|
22
|
+
),
|
23
|
+
]
|
@@ -0,0 +1,81 @@
|
|
1
|
+
import logging
|
2
|
+
|
3
|
+
from django.contrib.auth import get_user_model
|
4
|
+
from django.contrib.auth.hashers import make_password
|
5
|
+
|
6
|
+
from django_solomon.models import UserType
|
7
|
+
|
8
|
+
logger = logging.getLogger(__name__)
|
9
|
+
|
10
|
+
|
11
|
+
def get_user_by_email(email: str) -> UserType | None:
|
12
|
+
"""
|
13
|
+
Get a user by email.
|
14
|
+
|
15
|
+
This method supports both the standard User model and custom User models
|
16
|
+
by checking if the email field exists on the model.
|
17
|
+
|
18
|
+
Args:
|
19
|
+
email: The email to look up.
|
20
|
+
|
21
|
+
Returns:
|
22
|
+
The user if found, None otherwise.
|
23
|
+
"""
|
24
|
+
user_model = get_user_model()
|
25
|
+
|
26
|
+
# Check if the user model has an email field
|
27
|
+
if hasattr(user_model, "EMAIL_FIELD"):
|
28
|
+
email_field = user_model.EMAIL_FIELD
|
29
|
+
else:
|
30
|
+
email_field = "email"
|
31
|
+
|
32
|
+
# Make sure the email field exists on the model
|
33
|
+
if not hasattr(user_model, email_field):
|
34
|
+
logger.warning(f"User model {user_model.__name__} does not have field {email_field}, falling back to 'email'")
|
35
|
+
email_field = "email"
|
36
|
+
|
37
|
+
# Check if the fallback field exists
|
38
|
+
if not hasattr(user_model, email_field):
|
39
|
+
logger.error(f"User model {user_model.__name__} does not have an email field")
|
40
|
+
return None
|
41
|
+
|
42
|
+
# Query for the user
|
43
|
+
try:
|
44
|
+
query = {email_field: email}
|
45
|
+
return user_model.objects.get(**query)
|
46
|
+
except user_model.DoesNotExist:
|
47
|
+
return None
|
48
|
+
except Exception as e:
|
49
|
+
logger.error(f"Error getting user by email: {e}")
|
50
|
+
return None
|
51
|
+
|
52
|
+
|
53
|
+
def create_user_from_email(email: str) -> UserType:
|
54
|
+
"""
|
55
|
+
Creates a new user with the given email.
|
56
|
+
|
57
|
+
This function uses the email as both the username and email address
|
58
|
+
for the new user.
|
59
|
+
|
60
|
+
Args:
|
61
|
+
email: The email to use for the new user.
|
62
|
+
|
63
|
+
Returns:
|
64
|
+
The newly created user.
|
65
|
+
"""
|
66
|
+
user_model = get_user_model()
|
67
|
+
|
68
|
+
# Create the user with email as username
|
69
|
+
user_kwargs = {
|
70
|
+
"username": email,
|
71
|
+
"email": email,
|
72
|
+
"password": make_password(None),
|
73
|
+
}
|
74
|
+
|
75
|
+
# Check if the user model has an email field
|
76
|
+
if hasattr(user_model, "EMAIL_FIELD"):
|
77
|
+
email_field = user_model.EMAIL_FIELD
|
78
|
+
if email_field != "email":
|
79
|
+
user_kwargs[email_field] = email
|
80
|
+
|
81
|
+
return user_model.objects.create_user(**user_kwargs)
|
django_solomon/views.py
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
import logging
|
2
|
+
|
3
|
+
from django.conf import settings
|
4
|
+
from django.contrib.auth import get_user_model, authenticate, login
|
5
|
+
from django.core.mail import send_mail
|
6
|
+
from django.http import HttpRequest, HttpResponse
|
7
|
+
from django.shortcuts import redirect, render
|
8
|
+
from django.template.loader import render_to_string
|
9
|
+
from django.urls import reverse
|
10
|
+
from django.utils.translation import gettext_lazy as _
|
11
|
+
from mjml import mjml2html
|
12
|
+
|
13
|
+
from django_solomon.config import (
|
14
|
+
get_login_form_template,
|
15
|
+
get_invalid_magic_link_template,
|
16
|
+
get_magic_link_sent_template,
|
17
|
+
get_email_text_template,
|
18
|
+
get_email_mjml_template,
|
19
|
+
)
|
20
|
+
from django_solomon.forms import MagicLinkForm
|
21
|
+
from django_solomon.models import BlacklistedEmail, MagicLink
|
22
|
+
from django_solomon.utilities import get_user_by_email, create_user_from_email
|
23
|
+
|
24
|
+
logger = logging.getLogger(__name__)
|
25
|
+
User = get_user_model()
|
26
|
+
|
27
|
+
|
28
|
+
def send_magic_link(request: HttpRequest) -> HttpResponse:
|
29
|
+
"""
|
30
|
+
Handles the process of sending a magic link to a user via email. A magic link acts as a
|
31
|
+
temporary, secure token allowing the user to authenticate without a password. The endpoint
|
32
|
+
validates incoming requests, checks for blacklisted emails, retrieves a user based on the
|
33
|
+
submitted email, and sends a magic link if the user exists. Supports POST and GET methods.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
request (HttpRequest): The HTTP request object that includes metadata about the request.
|
37
|
+
|
38
|
+
Returns:
|
39
|
+
HttpResponse: A redirect response to the "magic_link_sent" page or renders the form template
|
40
|
+
for sending the magic link.
|
41
|
+
"""
|
42
|
+
if request.method == "POST":
|
43
|
+
form = MagicLinkForm(request.POST)
|
44
|
+
if form.is_valid():
|
45
|
+
email = form.cleaned_data["email"]
|
46
|
+
|
47
|
+
# Check if the email is blacklisted
|
48
|
+
if BlacklistedEmail.objects.filter(email=email).exists():
|
49
|
+
logger.info(f"Blocked magic link request for blacklisted email: {email}")
|
50
|
+
return redirect("django_solomon:magic_link_sent")
|
51
|
+
|
52
|
+
# Get user by email
|
53
|
+
user = get_user_by_email(email)
|
54
|
+
|
55
|
+
# Signup unknown users if enabled in settings
|
56
|
+
if not user and getattr(settings, "SOLOMON_CREATE_USER_IF_NOT_FOUND", False):
|
57
|
+
user = create_user_from_email(email)
|
58
|
+
|
59
|
+
# Send magic link if user exists
|
60
|
+
if user:
|
61
|
+
magic_link = MagicLink.objects.create_for_user(user)
|
62
|
+
link_url = request.build_absolute_uri(
|
63
|
+
reverse(
|
64
|
+
"django_solomon:validate_magic_link",
|
65
|
+
kwargs={"token": magic_link.token},
|
66
|
+
)
|
67
|
+
)
|
68
|
+
# Calculate expiry time in minutes
|
69
|
+
expiry_time = getattr(settings, "SOLOMON_LINK_EXPIRATION", 300) // 60 # Convert seconds to minutes
|
70
|
+
|
71
|
+
context = {
|
72
|
+
"magic_link_url": link_url,
|
73
|
+
"expiry_time": expiry_time,
|
74
|
+
}
|
75
|
+
|
76
|
+
text_message = render_to_string(get_email_text_template(), context).strip()
|
77
|
+
html_message = mjml2html(render_to_string(get_email_mjml_template(), context))
|
78
|
+
|
79
|
+
send_mail(
|
80
|
+
_("Your Magic Link"),
|
81
|
+
text_message,
|
82
|
+
getattr(settings, "DEFAULT_FROM_EMAIL", "noreply@example.com"),
|
83
|
+
[email],
|
84
|
+
fail_silently=False,
|
85
|
+
html_message=html_message,
|
86
|
+
)
|
87
|
+
|
88
|
+
return redirect("django_solomon:magic_link_sent")
|
89
|
+
else:
|
90
|
+
form = MagicLinkForm()
|
91
|
+
return render(request, get_login_form_template(), {"form": form})
|
92
|
+
|
93
|
+
|
94
|
+
def magic_link_sent(request: HttpRequest) -> HttpResponse:
|
95
|
+
"""
|
96
|
+
Renders a page that informs the user a magic link has been sent to their email.
|
97
|
+
|
98
|
+
This view function processes an incoming HTTP request and renders the HTML page
|
99
|
+
to indicate that the magic link email was successfully dispatched.
|
100
|
+
|
101
|
+
Args:
|
102
|
+
request: An HttpRequest object representing the user's request.
|
103
|
+
|
104
|
+
Returns:
|
105
|
+
An HttpResponse object with the rendered "magic_link_sent.html" template.
|
106
|
+
"""
|
107
|
+
return render(request, get_magic_link_sent_template())
|
108
|
+
|
109
|
+
|
110
|
+
def validate_magic_link(request: HttpRequest, token: str) -> HttpResponse:
|
111
|
+
"""
|
112
|
+
Validates a magic link for user authentication in a Django application. If the token is valid, the
|
113
|
+
user is authenticated and logged in. If the token is invalid or expired, an error message is
|
114
|
+
displayed. The function redirects authenticated users to a predetermined success URL.
|
115
|
+
|
116
|
+
Arguments:
|
117
|
+
request: The HTTP request object associated with the current request.
|
118
|
+
token: A string representing the token provided in the magic link.
|
119
|
+
|
120
|
+
Returns:
|
121
|
+
HttpResponse: An HTTP response redirecting the user to the success URL upon successful
|
122
|
+
authentication, or rendering an error page if authentication fails.
|
123
|
+
"""
|
124
|
+
user = authenticate(request=request, token=token)
|
125
|
+
|
126
|
+
if not user:
|
127
|
+
# Get the error message from the request object, or use a default message
|
128
|
+
error_message = getattr(request, "magic_link_error", _("Invalid or expired magic link"))
|
129
|
+
|
130
|
+
return render(
|
131
|
+
request,
|
132
|
+
get_invalid_magic_link_template(),
|
133
|
+
{"error": error_message},
|
134
|
+
)
|
135
|
+
|
136
|
+
# Log the user in
|
137
|
+
login(request, user)
|
138
|
+
|
139
|
+
# Redirect to the success URL
|
140
|
+
success_url = getattr(settings, "SOLOMON_LOGIN_REDIRECT_URL", settings.LOGIN_REDIRECT_URL)
|
141
|
+
return redirect(success_url)
|
@@ -0,0 +1,198 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: django-solomon
|
3
|
+
Version: 0.1.2
|
4
|
+
Project-URL: Home, https://django-solomon.rtfd.io/
|
5
|
+
Project-URL: Documentation, https://django-solomon.rtfd.io/
|
6
|
+
Project-URL: Repository, https://codeberg.org/oliverandrich/django-solomon
|
7
|
+
Author-email: Oliver Andrich <oliver@andrich.me>
|
8
|
+
License: MIT License
|
9
|
+
|
10
|
+
Copyright (c) 2025 Oliver Andrich
|
11
|
+
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
14
|
+
in the Software without restriction, including without limitation the rights
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
17
|
+
furnished to do so, subject to the following conditions:
|
18
|
+
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
20
|
+
copies or substantial portions of the Software.
|
21
|
+
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
28
|
+
SOFTWARE.
|
29
|
+
License-File: LICENSE
|
30
|
+
Keywords: django
|
31
|
+
Classifier: Development Status :: 3 - Alpha
|
32
|
+
Classifier: Environment :: Web Environment
|
33
|
+
Classifier: Framework :: Django :: 4.2
|
34
|
+
Classifier: Framework :: Django :: 5.0
|
35
|
+
Classifier: Framework :: Django :: 5.1
|
36
|
+
Classifier: Framework :: Django :: 5.2
|
37
|
+
Classifier: Intended Audience :: Developers
|
38
|
+
Classifier: License :: OSI Approved :: MIT License
|
39
|
+
Classifier: Operating System :: OS Independent
|
40
|
+
Classifier: Programming Language :: Python :: 3.10
|
41
|
+
Classifier: Programming Language :: Python :: 3.11
|
42
|
+
Classifier: Programming Language :: Python :: 3.12
|
43
|
+
Classifier: Programming Language :: Python :: 3.13
|
44
|
+
Classifier: Topic :: Software Development :: Libraries
|
45
|
+
Classifier: Topic :: Utilities
|
46
|
+
Requires-Python: >=3.10
|
47
|
+
Requires-Dist: django>=4.2
|
48
|
+
Requires-Dist: mjml-python>=1.3.5
|
49
|
+
Description-Content-Type: text/markdown
|
50
|
+
|
51
|
+
# django-solomon
|
52
|
+
|
53
|
+
[](https://pypi.org/project/django-solomon/)
|
54
|
+
[](https://pypi.org/project/django-solomon/)
|
55
|
+
[](https://pypi.org/project/django-solomon/)
|
56
|
+
[](https://django-solomon.rtfd.io/en/latest/?badge=latest)
|
57
|
+
|
58
|
+
A Django app for passwordless authentication using magic links.
|
59
|
+
|
60
|
+
## Features
|
61
|
+
|
62
|
+
- Passwordless authentication using magic links sent via email
|
63
|
+
- Configurable link expiration time
|
64
|
+
- Blacklist functionality to block specific email addresses
|
65
|
+
- Support for auto-creating users when they request a magic link
|
66
|
+
- Customizable templates for emails and pages
|
67
|
+
- Compatible with Django's authentication system
|
68
|
+
|
69
|
+
## Installation
|
70
|
+
|
71
|
+
```bash
|
72
|
+
pip install django-solomon
|
73
|
+
```
|
74
|
+
|
75
|
+
## Configuration
|
76
|
+
|
77
|
+
1. Add `django_solomon` to your `INSTALLED_APPS` in your Django settings:
|
78
|
+
|
79
|
+
```python
|
80
|
+
INSTALLED_APPS = [
|
81
|
+
# ...
|
82
|
+
'django_solomon',
|
83
|
+
# ...
|
84
|
+
]
|
85
|
+
```
|
86
|
+
|
87
|
+
2. Add the authentication backend to your settings:
|
88
|
+
|
89
|
+
```python
|
90
|
+
AUTHENTICATION_BACKENDS = [
|
91
|
+
'django_solomon.backends.MagicLinkBackend',
|
92
|
+
'django.contrib.auth.backends.ModelBackend', # Keep the default backend
|
93
|
+
]
|
94
|
+
```
|
95
|
+
|
96
|
+
3. Include the django-solomon URLs in your project's `urls.py`:
|
97
|
+
|
98
|
+
```python
|
99
|
+
from django.urls import include, path
|
100
|
+
|
101
|
+
urlpatterns = [
|
102
|
+
# ...
|
103
|
+
path('auth/', include('django_solomon.urls')),
|
104
|
+
# ...
|
105
|
+
]
|
106
|
+
```
|
107
|
+
|
108
|
+
4. Set the login URL in your settings to use django-solomon's login view:
|
109
|
+
|
110
|
+
```python
|
111
|
+
LOGIN_URL = 'django_solomon:login'
|
112
|
+
```
|
113
|
+
|
114
|
+
This ensures that when users need to authenticate, they'll be redirected to the magic link login page.
|
115
|
+
|
116
|
+
5. Configure your email settings to ensure emails can be sent:
|
117
|
+
|
118
|
+
```python
|
119
|
+
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
120
|
+
EMAIL_HOST = 'smtp.example.com'
|
121
|
+
EMAIL_PORT = 587
|
122
|
+
EMAIL_USE_TLS = True
|
123
|
+
EMAIL_HOST_USER = 'your-email@example.com'
|
124
|
+
EMAIL_HOST_PASSWORD = 'your-password'
|
125
|
+
DEFAULT_FROM_EMAIL = 'your-email@example.com'
|
126
|
+
```
|
127
|
+
|
128
|
+
## Settings
|
129
|
+
|
130
|
+
django-solomon provides several settings that you can customize in your Django settings file:
|
131
|
+
|
132
|
+
| Setting | Default | Description |
|
133
|
+
|---------------------------------------|-------------------------------------------------|----------------------------------------------------------------------------------------|
|
134
|
+
| `SOLOMON_LINK_EXPIRATION` | `300` | The expiration time for magic links in seconds |
|
135
|
+
| `SOLOMON_ONLY_ONE_LINK_ALLOWED` | `True` | If enabled, only one active magic link is allowed per user |
|
136
|
+
| `SOLOMON_CREATE_USER_IF_NOT_FOUND` | `False` | If enabled, creates a new user when a magic link is requested for a non-existent email |
|
137
|
+
| `SOLOMON_LOGIN_REDIRECT_URL` | `settings.LOGIN_REDIRECT_URL` | The URL to redirect to after successful authentication |
|
138
|
+
| `SOLOMON_ALLOW_ADMIN_LOGIN` | `True` | If enabled, allows superusers to log in using magic links |
|
139
|
+
| `SOLOMON_ALLOW_STAFF_LOGIN` | `True` | If enabled, allows staff users to log in using magic links |
|
140
|
+
| `SOLOMON_MAIL_TEXT_TEMPLATE` | `"django_solomon/email/magic_link.txt"` | The template to use for plain text magic link emails |
|
141
|
+
| `SOLOMON_MAIL_MJML_TEMPLATE` | `"django_solomon/email/magic_link.mjml"` | The template to use for HTML magic link emails (MJML format) |
|
142
|
+
| `SOLOMON_LOGIN_FORM_TEMPLATE` | `"django_solomon/base/login_form.html"` | The template to use for the login form page |
|
143
|
+
| `SOLOMON_INVALID_MAGIC_LINK_TEMPLATE` | `"django_solomon/base/invalid_magic_link.html"` | The template to use for the invalid magic link page |
|
144
|
+
| `SOLOMON_MAGIC_LINK_SENT_TEMPLATE` | `"django_solomon/base/magic_link_sent.html"` | The template to use for the magic link sent confirmation page |
|
145
|
+
|
146
|
+
## Usage
|
147
|
+
|
148
|
+
### Basic Usage
|
149
|
+
|
150
|
+
1. Direct users to the magic link request page at `/auth/magic-link/`
|
151
|
+
2. Users enter their email address
|
152
|
+
3. A magic link is sent to their email
|
153
|
+
4. Users click the link in their email
|
154
|
+
5. They are authenticated and redirected to the success URL
|
155
|
+
|
156
|
+
### Template Customization
|
157
|
+
|
158
|
+
You can override the default templates by creating your own versions in your project:
|
159
|
+
|
160
|
+
- `django_solomon/login_form.html` - The form to request a magic link
|
161
|
+
- `django_solomon/magic_link_sent.html` - The confirmation page after a magic link is sent
|
162
|
+
- `django_solomon/invalid_magic_link.html` - The error page for invalid magic links
|
163
|
+
- `django_solomon/email/magic_link.txt` - The plain text email template for the magic link
|
164
|
+
- `django_solomon/email/magic_link.mjml` - The HTML email template for the magic link (in MJML format)
|
165
|
+
|
166
|
+
Alternatively, you can specify custom templates using the settings variables:
|
167
|
+
|
168
|
+
- `SOLOMON_LOGIN_FORM_TEMPLATE` - Custom template for the login form
|
169
|
+
- `SOLOMON_MAGIC_LINK_SENT_TEMPLATE` - Custom template for the confirmation page
|
170
|
+
- `SOLOMON_INVALID_MAGIC_LINK_TEMPLATE` - Custom template for the error page
|
171
|
+
- `SOLOMON_MAIL_TEXT_TEMPLATE` - Custom template for the plain text email
|
172
|
+
- `SOLOMON_MAIL_MJML_TEMPLATE` - Custom template for the HTML email (MJML format)
|
173
|
+
|
174
|
+
### Programmatic Usage
|
175
|
+
|
176
|
+
You can also use django-solomon programmatically in your views:
|
177
|
+
|
178
|
+
```python
|
179
|
+
from django.contrib.auth import authenticate, login
|
180
|
+
from django_solomon.models import MagicLink
|
181
|
+
|
182
|
+
# Create a magic link for a user
|
183
|
+
magic_link = MagicLink.objects.create_for_user(user)
|
184
|
+
|
185
|
+
# Authenticate a user with a token
|
186
|
+
user = authenticate(request=request, token=token)
|
187
|
+
if user:
|
188
|
+
login(request, user)
|
189
|
+
```
|
190
|
+
|
191
|
+
## License
|
192
|
+
|
193
|
+
This software is licensed under [MIT license](./LICENSE).
|
194
|
+
|
195
|
+
## Contributing
|
196
|
+
|
197
|
+
Contributions are welcome! Please feel free to submit a Pull Request
|
198
|
+
on [Codeberg](https://codeberg.org/oliverandrich/django-solomon).
|
@@ -0,0 +1,26 @@
|
|
1
|
+
django_solomon/__init__.py,sha256=4GdjeQVyChzdc7pZ1jrpknjcnu9Do_0nSh42UZNICKo,80
|
2
|
+
django_solomon/admin.py,sha256=hwesCOQgUDDHPh5qq97AObfSyMgIOKsdzoa69naMPx4,1428
|
3
|
+
django_solomon/apps.py,sha256=glfjdSSzrxuwJ2FrzuF2C4oFNqikMqhSHHmca2qq5Vg,159
|
4
|
+
django_solomon/backends.py,sha256=A_TTNaduRmZOZF-R0C1yhs9QpXejBCRCz7wDcp_BupY,2403
|
5
|
+
django_solomon/config.py,sha256=JEl3cY0BnfC9ja0ZVeLmfIwUStbUAO6vRhGZrrig5Yw,787
|
6
|
+
django_solomon/forms.py,sha256=ymDsZ-KWyJfo12vGbZCxIhIvVket93xYR4MgDif4fh0,312
|
7
|
+
django_solomon/models.py,sha256=NRsN-F7Vnfx-l5cJgjAJqPhbhRIeIctcRh4GeuysUZA,4771
|
8
|
+
django_solomon/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
9
|
+
django_solomon/urls.py,sha256=tT-I-p6dA2rD1IvH3pAcJRkPXYW0oYmYWXZAoiLJzGY,471
|
10
|
+
django_solomon/utilities.py,sha256=Xna8jlr6E5YYxKx7Pz_xD6LwIm3ZfZGdyYfJpIoOD-g,2275
|
11
|
+
django_solomon/views.py,sha256=qPwpSXhKyLg9zXJVmzC4SJqpTwqVFLbwQdq9qWy2Im0,5553
|
12
|
+
django_solomon/locale/de/LC_MESSAGES/django.po,sha256=6lo72lsDV1bKBVowiRsQoaVKH5Mbkf34z78sG1LEYq4,747
|
13
|
+
django_solomon/locale/en/LC_MESSAGES/django.po,sha256=6MjGCWQn-1AjOnP_gXtLa0Oad4qfNAn5tXdhnw_wEcg,732
|
14
|
+
django_solomon/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
15
|
+
django_solomon/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
16
|
+
django_solomon/migrations/0001_initial.py,sha256=i-UhR1KQ4p7WmbDg9xUQfMmDo8yYdIigvHNsefLHJEc,1981
|
17
|
+
django_solomon/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
18
|
+
django_solomon/templates/django_solomon/base/invalid_magic_link.html,sha256=3U4T2n4uvPvktiOOHYVyNmk0aXFfE4DPrURE1DinWKk,554
|
19
|
+
django_solomon/templates/django_solomon/base/login_form.html,sha256=VYGnrno3c36W9f04XdRqJpjsDfOy0sq5D_cLayPz8Q0,546
|
20
|
+
django_solomon/templates/django_solomon/base/magic_link_sent.html,sha256=GIMxnw3E98TXVkVQkMRTHmX5mz0xUsvgXVj25gO2HPQ,461
|
21
|
+
django_solomon/templates/django_solomon/email/magic_link.mjml,sha256=OnfVGm2yFrOmoQ1syo6-Duq_Qraf3Tv0Vy5oidvt55g,1427
|
22
|
+
django_solomon/templates/django_solomon/email/magic_link.txt,sha256=yl01eie3U2HkFEJvB1Qlm8Z6FPuop5yubDXFY5V507Q,502
|
23
|
+
django_solomon-0.1.2.dist-info/METADATA,sha256=C-MpFkZUrPXXrKgQJO1Fs1WW5rIXXUXGcfw1RpeGEBU,8902
|
24
|
+
django_solomon-0.1.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
25
|
+
django_solomon-0.1.2.dist-info/licenses/LICENSE,sha256=tbfFOFdH5mHIfmNGo6KGOC3U8f-hTCLfetcr8A89WwI,1071
|
26
|
+
django_solomon-0.1.2.dist-info/RECORD,,
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Oliver Andrich
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|