feat: registration with email confirmation flows

wip-payments
Aravinth Manivannan 2022-06-10 22:34:57 +05:30
parent 583a65bc18
commit 453b115485
Signed by: realaravinth
GPG Key ID: AD9F0F08E855ED88
10 changed files with 316 additions and 12 deletions

View File

@ -0,0 +1,43 @@
# Generated by Django 4.0.3 on 2022-06-10 16:09
import accounts.utils
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="AccountConfirmChallenge",
fields=[
(
"challenge_text",
models.CharField(
blank=True,
default=accounts.utils.gen_secret,
editable=False,
max_length=32,
primary_key=True,
serialize=False,
unique=True,
verbose_name="Challenge text",
),
),
(
"owned_by",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
),
]

View File

@ -0,0 +1,25 @@
# Generated by Django 4.0.3 on 2022-06-10 16:28
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("accounts", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="accountconfirmchallenge",
name="owned_by",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
unique=True,
),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 4.0.3 on 2022-06-10 16:28
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("accounts", "0002_alter_accountconfirmchallenge_owned_by"),
]
operations = [
migrations.AlterField(
model_name="accountconfirmchallenge",
name="owned_by",
field=models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
]

View File

@ -0,0 +1,38 @@
# Generated by Django 4.0.3 on 2022-06-10 16:44
import accounts.utils
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0003_alter_accountconfirmchallenge_owned_by"),
]
operations = [
migrations.AddField(
model_name="accountconfirmchallenge",
name="public_ref",
field=models.CharField(
default=accounts.utils.gen_secret,
editable=False,
max_length=32,
unique=True,
verbose_name="Public referece to challenge text",
),
),
migrations.AlterField(
model_name="accountconfirmchallenge",
name="challenge_text",
field=models.CharField(
default=accounts.utils.gen_secret,
editable=False,
max_length=32,
primary_key=True,
serialize=False,
unique=True,
verbose_name="Challenge text",
),
),
]

View File

@ -1,3 +1,50 @@
from django.db import models
# Copyright © 2022 Aravinth Manivannan <realaravinth@batsense.net>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Create your models here.
from django.db import models
from django.contrib.auth.models import User
from django.utils.http import urlencode
from django.urls import reverse
from .utils import gen_secret
class AccountConfirmChallenge(models.Model):
owned_by = models.OneToOneField(User, on_delete=models.CASCADE)
public_ref = models.CharField(
"Public referece to challenge text",
unique=True,
max_length=32,
default=gen_secret,
editable=False,
)
challenge_text = models.CharField(
"Challenge text",
unique=True,
max_length=32,
default=gen_secret,
editable=False,
primary_key=True,
)
def __str__(self):
return f"{self.challenge_text}"
def verification_link(self):
"""
Get verification link
"""
return reverse("accounts.verify", args=(self.challenge_text,))

View File

@ -9,7 +9,7 @@
<nav>
<ul>
<li><a href="{% url 'accounts.login' %}">Login</a></li>
<li><a href="/o/applications/">New App</a></li>
<li><a href="{% url 'accounts.logout' %}">Logout</a></li>
</ul>
</nav>
</body>

View File

@ -15,11 +15,32 @@
from django.contrib import admin
from django.urls import path, include
from .views import login_view, public_view, logout_view, protected_view
from .views import (
login_view,
logout_view,
protected_view,
register_view,
default_login_url,
verify_account,
resend_verification_email_view,
verification_pending_view,
)
urlpatterns = [
path("login/", login_view, name="accounts.login"),
path("register/", register_view, name="accounts.register"),
path("logout/", login_view, name="accounts.logout"),
path("protected/", protected_view, name="accounts.protected"),
path("", public_view, name="accounts.public"),
path("accounts/login/", default_login_url, name="accounts.default_login_url"),
path(
"accounts/verify/pending/<str:public_ref>/",
verification_pending_view,
name="accounts.verify.pending",
),
path(
"accounts/verify/resend/<str:public_ref>/",
resend_verification_email_view,
name="accounts.verify.resend",
),
path("accounts/verify/<str:challenge>/", verify_account, name="accounts.verify"),
path("", protected_view, name="accounts.home"),
]

View File

@ -12,7 +12,8 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.shortcuts import render, redirect
from django.shortcuts import render, redirect, get_object_or_404
from django.utils.http import urlencode
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import login_required
@ -21,6 +22,10 @@ from django.views.decorators.csrf import csrf_protect
from django.urls import reverse
from .models import AccountConfirmChallenge
from .utils import send_verification_email
@csrf_protect
def login_view(request):
if request.method == "GET":
@ -49,7 +54,7 @@ def login_view(request):
next_url = request.POST["next"]
if next_url:
return redirect(next_url)
return redirect(reverse("accounts.protected"))
return redirect(reverse("accounts.home"))
ctx = {
"error": {
@ -65,11 +70,104 @@ def protected_view(request):
return render(request, "accounts/protected.html")
def default_login_url(request):
ctx = {"next": request.GET["next"]}
return redirect(f"{reverse('accounts.login')}?{urlencode(ctx)}")
@login_required
def logout_view(request):
logout(request)
return redirect(reverse("accounts.login"))
def public_view(request):
return render(request, "accounts/public.html")
def register_view(request):
if request.method == "GET":
ctx = {}
if "next" in request.GET:
ctx["next"] = request.GET["next"]
return render(request, "accounts/auth/register.html", ctx)
confirm_password = request.POST["confirm_password"]
password = request.POST["password"]
username = request.POST["username"]
email = request.POST["email"]
if password != confirm_password:
ctx = {
"error": {
"title": "Registration Failed",
"reason": "Passwords don't match, please verify input",
},
"usernme": username,
"email": email,
}
return render(request, "accounts/auth/register.html", status=400, context=ctx)
User = get_user_model()
if User.objects.filter(email=email).exists():
ctx = {
"error": {
"title": "Registration Failed",
"reason": "Email is already registered",
},
"usernme": username,
"email": email,
}
return render(request, "accounts/auth/register.html", status=400, context=ctx)
if User.objects.filter(username=username).exists():
ctx = {
"error": {
"title": "Registration Failed",
"reason": "Username is already registered",
},
"usernme": username,
"email": email,
}
return render(request, "accounts/auth/register.html", status=400, context=ctx)
user = get_user_model().objects.create_user(
username=username,
email=email,
is_active=False,
password=password,
) # TODO: get email from settings.py
challenge = AccountConfirmChallenge(owned_by=user)
challenge.save()
send_verification_email(request, challenge=challenge)
return redirect(reverse("accounts.verify.pending", args=(challenge.public_ref,)))
def verification_pending_view(request, public_ref):
challenge = get_object_or_404(AccountConfirmChallenge, public_ref=public_ref)
ctx = {
"email": challenge.owned_by.email,
"public_ref": challenge.public_ref,
}
return render(request, "accounts/auth/verification-pending.html", context=ctx)
def resend_verification_email_view(request, public_ref):
challenge = get_object_or_404(AccountConfirmChallenge, public_ref=public_ref)
send_verification_email(request, challenge=challenge)
return redirect(reverse("accounts.verify.pending", args=(challenge.public_ref,)))
def verify_account(request, challenge):
challenge = get_object_or_404(AccountConfirmChallenge, challenge_text=challenge)
if request.method == "GET":
ctx = {
"challenge": challenge,
}
return render(request, "accounts/auth/verify.html", context=ctx)
challenge.owned_by.is_active = True
challenge.owned_by.save()
challenge.delete()
return redirect("accounts.login")

View File

@ -115,7 +115,6 @@ USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.0/howto/static-files/
@ -130,10 +129,13 @@ STATICFILES_DIRS = [
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
### Dashbaord specific configuration options
HOSTEA = {
"RESTRICT_NEW_INTEGRATION_INSTALLATION": True,
"INSTANCE_MAINTAINER_CONTACT": "contact@hostea.example.org",
}
EMAIL_CONFIG = env.email("EMAIL_URL", default="smtp://admin:password@localhost:10025")
vars().update(EMAIL_CONFIG)

View File

@ -386,6 +386,13 @@ pre {
box-sizing: border-box;
}
.dialogue-box__container {
width: 30%;
display: flex;
align-items: center;
flex-direction: column;
}
/* footer starts */
footer {