Add signup and password reset
This commit is contained in:
parent
2a3690d1c1
commit
6adfdbabe0
|
@ -0,0 +1,28 @@
|
||||||
|
# Generated by Django 4.1.3 on 2022-11-18 01:40
|
||||||
|
|
||||||
|
import functools
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import core.uploads
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("activities", "0008_postattachment"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="postattachment",
|
||||||
|
name="file",
|
||||||
|
field=models.FileField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
upload_to=functools.partial(
|
||||||
|
core.uploads.upload_namer, *("attachments",), **{}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -9,4 +9,7 @@ class CoreConfig(AppConfig):
|
||||||
name = "core"
|
name = "core"
|
||||||
|
|
||||||
def ready(self) -> None:
|
def ready(self) -> None:
|
||||||
|
from core.models import Config
|
||||||
|
|
||||||
|
Config.system = Config.load_system()
|
||||||
jsonld.set_document_loader(builtin_document_loader)
|
jsonld.set_document_loader(builtin_document_loader)
|
||||||
|
|
|
@ -3,7 +3,7 @@ from core.models import Config
|
||||||
|
|
||||||
def config_context(request):
|
def config_context(request):
|
||||||
return {
|
return {
|
||||||
"config": Config.load_system(),
|
"config": Config.system,
|
||||||
"config_identity": (
|
"config_identity": (
|
||||||
Config.load_identity(request.identity) if request.identity else None
|
Config.load_identity(request.identity) if request.identity else None
|
||||||
),
|
),
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
# Generated by Django 4.1.3 on 2022-11-18 01:40
|
||||||
|
|
||||||
|
import functools
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import core.uploads
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("core", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="config",
|
||||||
|
name="image",
|
||||||
|
field=models.ImageField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
upload_to=functools.partial(
|
||||||
|
core.uploads.upload_namer, *("config",), **{}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -5,7 +5,6 @@ import pydantic
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.templatetags.static import static
|
from django.templatetags.static import static
|
||||||
from django.utils.functional import classproperty
|
|
||||||
|
|
||||||
from core.uploads import upload_namer
|
from core.uploads import upload_namer
|
||||||
from takahe import __version__
|
from takahe import __version__
|
||||||
|
@ -54,11 +53,6 @@ class Config(models.Model):
|
||||||
("key", "user", "identity"),
|
("key", "user", "identity"),
|
||||||
]
|
]
|
||||||
|
|
||||||
@classproperty
|
|
||||||
def system(cls):
|
|
||||||
cls.system = cls.load_system()
|
|
||||||
return cls.system
|
|
||||||
|
|
||||||
system: ClassVar["Config.ConfigOptions"] # type: ignore
|
system: ClassVar["Config.ConfigOptions"] # type: ignore
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -160,13 +154,16 @@ class Config(models.Model):
|
||||||
|
|
||||||
version: str = __version__
|
version: str = __version__
|
||||||
|
|
||||||
site_name: str = "takahē"
|
site_name: str = "Takahē"
|
||||||
highlight_color: str = "#449c8c"
|
highlight_color: str = "#449c8c"
|
||||||
site_about: str = "<h2>Welcome!</h2>\n\nThis is a community running Takahē."
|
site_about: str = "<h2>Welcome!</h2>\n\nThis is a community running Takahē."
|
||||||
site_icon: UploadedImage = static("img/icon-128.png")
|
site_icon: UploadedImage = static("img/icon-128.png")
|
||||||
site_banner: UploadedImage = static("img/fjords-banner-600.jpg")
|
site_banner: UploadedImage = static("img/fjords-banner-600.jpg")
|
||||||
|
|
||||||
|
allow_signups: bool = False
|
||||||
|
|
||||||
post_length: int = 500
|
post_length: int = 500
|
||||||
|
identity_max_per_user: int = 5
|
||||||
identity_max_age: int = 24 * 60 * 60
|
identity_max_age: int = 24 * 60 * 60
|
||||||
|
|
||||||
class UserOptions(pydantic.BaseModel):
|
class UserOptions(pydantic.BaseModel):
|
||||||
|
|
|
@ -136,6 +136,7 @@ header .logo {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
background: var(--color-highlight);
|
background: var(--color-highlight);
|
||||||
border-radius: 5px 0 0 0;
|
border-radius: 5px 0 0 0;
|
||||||
|
text-transform: lowercase;
|
||||||
padding: 10px 11px 9px 10px;
|
padding: 10px 11px 9px 10px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
font-size: 130%;
|
font-size: 130%;
|
||||||
|
@ -198,6 +199,12 @@ header menu a.identity {
|
||||||
width: 250px;
|
width: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
header menu a.identity i {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
padding: 0 7px 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
header menu a img {
|
header menu a img {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
@ -287,7 +294,7 @@ nav a i {
|
||||||
|
|
||||||
/* Icon menus */
|
/* Icon menus */
|
||||||
|
|
||||||
.icon-menu>a {
|
.icon-menu .option {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0px 0 20px 0;
|
margin: 0px 0 20px 0;
|
||||||
background: var(--color-bg-box);
|
background: var(--color-bg-box);
|
||||||
|
@ -299,19 +306,28 @@ nav a i {
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-menu>a:hover {
|
.icon-menu .option:hover {
|
||||||
border: 2px solid var(--color-highlight);
|
border: 2px solid var(--color-highlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-menu>a img,
|
.icon-menu .option.empty {
|
||||||
.icon-menu>a i {
|
color: var(--color-text-dull);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-menu .option.empty:hover {
|
||||||
|
border: 0;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-menu .option img,
|
||||||
|
.icon-menu .option i {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin: 0 10px 3px 0;
|
margin: 0 10px 3px 0;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-menu>a i {
|
.icon-menu .option i {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
|
|
|
@ -108,6 +108,11 @@ STATICFILES_DIRS = [
|
||||||
|
|
||||||
ALLOWED_HOSTS = ["*"]
|
ALLOWED_HOSTS = ["*"]
|
||||||
|
|
||||||
|
MAIN_DOMAIN = os.environ["TAKAHE_MAIN_DOMAIN"]
|
||||||
|
if "/" in MAIN_DOMAIN:
|
||||||
|
print("TAKAHE_MAIN_DOMAIN should be just the domain name - no https:// or path")
|
||||||
|
|
||||||
|
EMAIL_FROM = os.environ["TAKAHE_EMAIL_FROM"]
|
||||||
|
|
||||||
# Note that this MUST be a fully qualified URL in production
|
# Note that this MUST be a fully qualified URL in production
|
||||||
MEDIA_URL = os.environ.get("TAKAHE_MEDIA_URL", "/media/")
|
MEDIA_URL = os.environ.get("TAKAHE_MEDIA_URL", "/media/")
|
||||||
|
|
|
@ -16,3 +16,5 @@ CSRF_TRUSTED_ORIGINS = [
|
||||||
"http://127.0.0.1:8000",
|
"http://127.0.0.1:8000",
|
||||||
"https://127.0.0.1:8000",
|
"https://127.0.0.1:8000",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||||
|
|
|
@ -82,8 +82,10 @@ urlpatterns = [
|
||||||
path("@<handle>/posts/<int:post_id>/boost/", posts.Boost.as_view()),
|
path("@<handle>/posts/<int:post_id>/boost/", posts.Boost.as_view()),
|
||||||
path("@<handle>/posts/<int:post_id>/unboost/", posts.Boost.as_view(undo=True)),
|
path("@<handle>/posts/<int:post_id>/unboost/", posts.Boost.as_view(undo=True)),
|
||||||
# Authentication
|
# Authentication
|
||||||
path("auth/login/", auth.Login.as_view()),
|
path("auth/login/", auth.Login.as_view(), name="login"),
|
||||||
path("auth/logout/", auth.Logout.as_view()),
|
path("auth/logout/", auth.Logout.as_view(), name="logout"),
|
||||||
|
path("auth/signup/", auth.Signup.as_view(), name="signup"),
|
||||||
|
path("auth/reset/<token>/", auth.Reset.as_view(), name="password_reset"),
|
||||||
# Identity selection
|
# Identity selection
|
||||||
path("@<handle>/activate/", identity.ActivateIdentity.as_view()),
|
path("@<handle>/activate/", identity.ActivateIdentity.as_view()),
|
||||||
path("identity/select/", identity.SelectIdentity.as_view()),
|
path("identity/select/", identity.SelectIdentity.as_view()),
|
||||||
|
|
|
@ -23,11 +23,11 @@
|
||||||
<i class="fa-solid fa-gear"></i> Settings
|
<i class="fa-solid fa-gear"></i> Settings
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="/local/" {% if current_page == "local" %}class="selected"{% endif %}>
|
<a href="{% url "local" %}" {% if current_page == "local" %}class="selected"{% endif %}>
|
||||||
<i class="fa-solid fa-city"></i> Local Posts
|
<i class="fa-solid fa-city"></i> Local Posts
|
||||||
</a>
|
</a>
|
||||||
<h3></h3>
|
<h3></h3>
|
||||||
<a href="/auth/signup/" {% if current_page == "signup" %}class="selected"{% endif %}>
|
<a href="{% url "signup" %}" {% if current_page == "signup" %}class="selected"{% endif %}>
|
||||||
<i class="fa-solid fa-user-plus"></i> Create Account
|
<i class="fa-solid fa-user-plus"></i> Create Account
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Reset Password{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form action="." method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<fieldset>
|
||||||
|
<legend>Reset Password</legend>
|
||||||
|
<p>You are resetting your password for {{ reset.user.email }}.</p>
|
||||||
|
<p>Please choose your new password below.</p>
|
||||||
|
{% for field in form %}
|
||||||
|
{% include "forms/_field.html" %}
|
||||||
|
{% endfor %}
|
||||||
|
</fieldset>
|
||||||
|
<div class="buttons">
|
||||||
|
<button>Reset</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,14 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Password Reset{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Password Reset</legend>
|
||||||
|
<p>
|
||||||
|
Your password for <tt>{{ email }}</tt> has been reset!
|
||||||
|
</p>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,18 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Create Account{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form action="." method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<fieldset>
|
||||||
|
<legend>Create An Account</legend>
|
||||||
|
{% for field in form %}
|
||||||
|
{% include "forms/_field.html" %}
|
||||||
|
{% endfor %}
|
||||||
|
</fieldset>
|
||||||
|
<div class="buttons">
|
||||||
|
<button>Create</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,15 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Email Sent{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Email Sent</legend>
|
||||||
|
<p>
|
||||||
|
An email has been sent to <tt>{{ email }}</tt> - please follow
|
||||||
|
the link inside to finish creating your account.
|
||||||
|
</p>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,8 @@
|
||||||
|
Your email address was used to create a new account at {{config.site_name}} (https://{{settings.MAIN_DOMAIN}}).
|
||||||
|
|
||||||
|
To confirm your new account, go to this link:
|
||||||
|
|
||||||
|
https://{{settings.MAIN_DOMAIN}}/auth/reset/{{reset.token}}/
|
||||||
|
|
||||||
|
If this was not you, then please ignore this message - your email will not be
|
||||||
|
used to make an account if this link is not visited.
|
|
@ -0,0 +1,8 @@
|
||||||
|
A password reset was requested for your account ({{reset.user.email}}) at {{Config.system.site_name}} (https://{{settings.MAIN_DOMAIN}}).
|
||||||
|
|
||||||
|
To reset your password, go to this link:
|
||||||
|
|
||||||
|
https://{{settings.MAIN_DOMAIN}}/auth/reset/{{reset.token}}/
|
||||||
|
|
||||||
|
If this was not you, then please ignore this message - your password will not be
|
||||||
|
reset if this link is not visited.
|
|
@ -1,6 +1,14 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from users.models import Domain, Follow, Identity, InboxMessage, User, UserEvent
|
from users.models import (
|
||||||
|
Domain,
|
||||||
|
Follow,
|
||||||
|
Identity,
|
||||||
|
InboxMessage,
|
||||||
|
PasswordReset,
|
||||||
|
User,
|
||||||
|
UserEvent,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Domain)
|
@admin.register(Domain)
|
||||||
|
@ -42,6 +50,12 @@ class FollowAdmin(admin.ModelAdmin):
|
||||||
raw_id_fields = ["source", "target"]
|
raw_id_fields = ["source", "target"]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(PasswordReset)
|
||||||
|
class PasswordResetAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ["id", "user", "created"]
|
||||||
|
raw_id_fields = ["user"]
|
||||||
|
|
||||||
|
|
||||||
@admin.register(InboxMessage)
|
@admin.register(InboxMessage)
|
||||||
class InboxMessageAdmin(admin.ModelAdmin):
|
class InboxMessageAdmin(admin.ModelAdmin):
|
||||||
list_display = ["id", "state", "state_attempted", "message_type", "message_actor"]
|
list_display = ["id", "state", "state_attempted", "message_type", "message_actor"]
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
# Generated by Django 4.1.3 on 2022-11-18 01:40
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import stator.models
|
||||||
|
import users.models.password_reset
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("users", "0003_user_last_seen_alter_identity_domain"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="PasswordReset",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("state_ready", models.BooleanField(default=True)),
|
||||||
|
("state_changed", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("state_attempted", models.DateTimeField(blank=True, null=True)),
|
||||||
|
("state_locked_until", models.DateTimeField(blank=True, null=True)),
|
||||||
|
(
|
||||||
|
"state",
|
||||||
|
stator.models.StateField(
|
||||||
|
choices=[("new", "new"), ("sent", "sent")],
|
||||||
|
default="new",
|
||||||
|
graph=users.models.password_reset.PasswordResetStates,
|
||||||
|
max_length=100,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("token", models.CharField(max_length=500, unique=True)),
|
||||||
|
("new_account", models.BooleanField()),
|
||||||
|
("created", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated", models.DateTimeField(auto_now=True)),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="password_resets",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -3,5 +3,6 @@ from .domain import Domain # noqa
|
||||||
from .follow import Follow, FollowStates # noqa
|
from .follow import Follow, FollowStates # noqa
|
||||||
from .identity import Identity, IdentityStates # noqa
|
from .identity import Identity, IdentityStates # noqa
|
||||||
from .inbox_message import InboxMessage, InboxMessageStates # noqa
|
from .inbox_message import InboxMessage, InboxMessageStates # noqa
|
||||||
|
from .password_reset import PasswordReset # noqa
|
||||||
from .user import User # noqa
|
from .user import User # noqa
|
||||||
from .user_event import UserEvent # noqa
|
from .user_event import UserEvent # noqa
|
||||||
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
|
||||||
|
from asgiref.sync import sync_to_async
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.mail import send_mail
|
||||||
|
from django.db import models
|
||||||
|
from django.template.loader import render_to_string
|
||||||
|
|
||||||
|
from core.models import Config
|
||||||
|
from stator.models import State, StateField, StateGraph, StatorModel
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordResetStates(StateGraph):
|
||||||
|
new = State(try_interval=3)
|
||||||
|
sent = State()
|
||||||
|
|
||||||
|
new.transitions_to(sent)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def handle_new(cls, instance: "PasswordReset"):
|
||||||
|
"""
|
||||||
|
Sends the password reset email.
|
||||||
|
"""
|
||||||
|
reset = await instance.afetch_full()
|
||||||
|
if reset.new_account:
|
||||||
|
await sync_to_async(send_mail)(
|
||||||
|
subject=f"{Config.system.site_name}: Confirm new account",
|
||||||
|
message=render_to_string(
|
||||||
|
"emails/new_account.txt",
|
||||||
|
{
|
||||||
|
"reset": reset,
|
||||||
|
"config": Config.system,
|
||||||
|
"settings": settings,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
from_email=settings.EMAIL_FROM,
|
||||||
|
recipient_list=[reset.user.email],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await sync_to_async(send_mail)(
|
||||||
|
subject=f"{Config.system.site_name}: Reset password",
|
||||||
|
message=render_to_string(
|
||||||
|
"emails/password_reset.txt",
|
||||||
|
{
|
||||||
|
"reset": reset,
|
||||||
|
"config": Config.system,
|
||||||
|
"settings": settings,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
from_email=settings.EMAIL_FROM,
|
||||||
|
recipient_list=[reset.user.email],
|
||||||
|
)
|
||||||
|
return cls.sent
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordReset(StatorModel):
|
||||||
|
"""
|
||||||
|
A password reset for a user (this is also how we create accounts)
|
||||||
|
"""
|
||||||
|
|
||||||
|
state = StateField(PasswordResetStates)
|
||||||
|
|
||||||
|
user = models.ForeignKey(
|
||||||
|
"users.user",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="password_resets",
|
||||||
|
)
|
||||||
|
|
||||||
|
token = models.CharField(max_length=500, unique=True)
|
||||||
|
new_account = models.BooleanField()
|
||||||
|
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_for_user(cls, user):
|
||||||
|
return cls.objects.create(
|
||||||
|
user=user,
|
||||||
|
token="".join(random.choice(string.ascii_lowercase) for i in range(42)),
|
||||||
|
new_account=not user.password,
|
||||||
|
)
|
||||||
|
|
||||||
|
### Async helpers ###
|
||||||
|
|
||||||
|
async def afetch_full(self):
|
||||||
|
"""
|
||||||
|
Returns a version of the object with all relations pre-loaded
|
||||||
|
"""
|
||||||
|
return await PasswordReset.objects.select_related(
|
||||||
|
"user",
|
||||||
|
).aget(pk=self.pk)
|
|
@ -62,6 +62,10 @@ class BasicPage(AdminSettingsPage):
|
||||||
"title": "Site Banner",
|
"title": "Site Banner",
|
||||||
"help_text": "Must be at least 650px wide. 3:1 ratio of width:height recommended.",
|
"help_text": "Must be at least 650px wide. 3:1 ratio of width:height recommended.",
|
||||||
},
|
},
|
||||||
|
"identity_max_per_user": {
|
||||||
|
"title": "Maximum Identities Per User",
|
||||||
|
"help_text": "Non-admins will be blocked from creating more than this",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
layout = {
|
layout = {
|
||||||
|
@ -73,6 +77,7 @@ class BasicPage(AdminSettingsPage):
|
||||||
"highlight_color",
|
"highlight_color",
|
||||||
],
|
],
|
||||||
"Posts": ["post_length"],
|
"Posts": ["post_length"],
|
||||||
|
"Identities": ["identity_max_per_user"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
|
from django import forms
|
||||||
|
from django.contrib.auth.password_validation import validate_password
|
||||||
from django.contrib.auth.views import LoginView, LogoutView
|
from django.contrib.auth.views import LoginView, LogoutView
|
||||||
|
from django.shortcuts import get_object_or_404, render
|
||||||
|
from django.views.generic import FormView
|
||||||
|
|
||||||
|
from users.models import PasswordReset, User
|
||||||
|
|
||||||
|
|
||||||
class Login(LoginView):
|
class Login(LoginView):
|
||||||
|
@ -8,3 +14,78 @@ class Login(LoginView):
|
||||||
|
|
||||||
class Logout(LogoutView):
|
class Logout(LogoutView):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Signup(FormView):
|
||||||
|
|
||||||
|
template_name = "auth/signup.html"
|
||||||
|
|
||||||
|
class form_class(forms.Form):
|
||||||
|
|
||||||
|
email = forms.EmailField(
|
||||||
|
help_text="We will send a link to this email to set your password and create your account",
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean_email(self):
|
||||||
|
email = self.cleaned_data.get("email").lower()
|
||||||
|
if not email:
|
||||||
|
return
|
||||||
|
if User.objects.filter(email=email).exists():
|
||||||
|
raise forms.ValidationError("This email already has an account")
|
||||||
|
return email
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
user = User.objects.create(email=form.cleaned_data["email"])
|
||||||
|
PasswordReset.create_for_user(user)
|
||||||
|
return render(
|
||||||
|
self.request,
|
||||||
|
"auth/signup_success.html",
|
||||||
|
{"email": user.email},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Reset(FormView):
|
||||||
|
|
||||||
|
template_name = "auth/reset.html"
|
||||||
|
|
||||||
|
class form_class(forms.Form):
|
||||||
|
|
||||||
|
password = forms.CharField(
|
||||||
|
widget=forms.PasswordInput,
|
||||||
|
help_text="Must be at least 8 characters, and contain both letters and numbers.",
|
||||||
|
)
|
||||||
|
|
||||||
|
repeat_password = forms.CharField(
|
||||||
|
widget=forms.PasswordInput,
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean_password(self):
|
||||||
|
password = self.cleaned_data["password"]
|
||||||
|
validate_password(password)
|
||||||
|
return password
|
||||||
|
|
||||||
|
def clean_repeat_password(self):
|
||||||
|
if self.cleaned_data.get("password") != self.cleaned_data.get(
|
||||||
|
"repeat_password"
|
||||||
|
):
|
||||||
|
raise forms.ValidationError("Passwords do not match")
|
||||||
|
return self.cleaned_data.get("repeat_password")
|
||||||
|
|
||||||
|
def dispatch(self, request, token):
|
||||||
|
self.reset = get_object_or_404(PasswordReset, token=token)
|
||||||
|
return super().dispatch(request)
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
self.reset.user.set_password(form.cleaned_data["password"])
|
||||||
|
self.reset.user.save()
|
||||||
|
self.reset.delete()
|
||||||
|
return render(
|
||||||
|
self.request,
|
||||||
|
"auth/reset_success.html",
|
||||||
|
{"email": self.reset.user.email},
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_context_data(self, *args, **kwargs):
|
||||||
|
context = super().get_context_data(*args, **kwargs)
|
||||||
|
context["reset"] = self.reset
|
||||||
|
return context
|
||||||
|
|
Loading…
Reference in New Issue