initial commit

This commit is contained in:
2021-02-24 20:13:54 -08:00
commit dfeca6a325
50 changed files with 1467 additions and 0 deletions

0
accounts/__init__.py Normal file
View File

3
accounts/admin.py Normal file
View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

5
accounts/apps.py Normal file
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
name = 'accounts'

58
accounts/enable_totp.py Normal file
View File

@@ -0,0 +1,58 @@
from django.shortcuts import redirect, render
import qrcode.image.svg
from .forms import EnableTotpForm
from django.contrib import messages
from .models import Account
from io import BytesIO
import pyotp
import qrcode
def disable_totp(request):
if not request.user.is_authenticated:
return redirect('audio:home')
if not request.user.account.use_totp:
return redirect('audio:home')
if request.method == "POST":
account = Account.objects.get(user=request.user)
account.use_totp = False
account.totp_key = None
account.save()
messages.success(request, 'Thanks for disabling 2fa!', extra_tags="mb-0")
return(redirect('accounts:edit_profile'))
return render(request, 'confirmation.html', {})
def enable_totp(request):
if not request.user.is_authenticated:
return redirect('audio:home')
qr = get_totp_qr(request.user)
if request.method == "POST":
form = EnableTotpForm(request.POST, instance=request.user.account)
if form.is_valid():
totp_code = form.cleaned_data['totp_code']
if pyotp.TOTP(request.user.account.totp_key).verify(int(totp_code), valid_window=5):
account = Account.objects.get(user=request.user)
account.use_totp = True
account.save()
messages.success(request, 'Thanks for enabling 2fa!', extra_tags="mb-0")
return(redirect('accounts:edit_profile'))
else:
messages.error(request, 'Wrong Code, try again?', extra_tags="mb-0")
else:
form = EnableTotpForm(instance=request.user.account)
return render(request, 'accounts/totp_form.html', {'form': form, 'qr': qr})
def get_totp_qr(user):
if user.account.totp_key is None:
account = Account.objects.get(user=user)
account.totp_key = pyotp.random_base32()
account.save()
user.account.totp_key = account.totp_key
totp_uri = pyotp.totp.TOTP(user.account.totp_key).provisioning_uri(name='audio', issuer_name='trentpalmer.org')
img = qrcode.make(totp_uri, image_factory=qrcode.image.svg.SvgPathImage)
f = BytesIO()
img.save(f)
return(f.getvalue().decode('utf-8'))

59
accounts/forms.py Normal file
View File

@@ -0,0 +1,59 @@
from django.contrib.auth.forms import ValidationError, UsernameField # , UserCreationForm
from django.contrib.auth.models import User
from django import forms
from .models import Account
class EnableTotpForm(forms.ModelForm):
totp_code = forms.CharField(max_length=6)
class Meta:
model = Account
fields = ("totp_code", )
class EditProfileForm(forms.Form):
email = forms.EmailField(
required=True,
label='Email',
max_length=254,
widget=forms.EmailInput(attrs={'autocomplete': 'email'})
)
first_name = UsernameField(required=False)
last_name = UsernameField(required=False)
password = forms.CharField(
label="confirm password",
strip=False,
widget=forms.PasswordInput(attrs={'autocomplete': 'current-password'}),
)
def __init__(self, user, *args, **kwargs):
self.user = user
super(EditProfileForm, self).__init__(*args, **kwargs)
def clean(self):
email = self.cleaned_data.get('email')
first_name = self.cleaned_data.get('first_name')
last_name = self.cleaned_data.get('last_name')
password = self.cleaned_data["password"]
if not self.user.check_password(password):
raise ValidationError("password is incorrect.")
if email != self.user.email:
if User.objects.filter(email=email).exists():
raise ValidationError("An account already exists with this email address.")
return {
'email': email,
'first_name': first_name,
'last_name': last_name,
}
def save(self, commit=True):
self.user.email = self.cleaned_data['email']
self.user.first_name = self.cleaned_data['first_name']
self.user.last_name = self.cleaned_data['last_name']
if commit:
self.user.save()
return self.user

58
accounts/login.py Normal file
View File

@@ -0,0 +1,58 @@
from django.shortcuts import render, redirect
from django.contrib.auth.forms import AuthenticationForm
from .forms import EnableTotpForm
from django.contrib.auth import login
from .models import Account
from django.contrib.auth.models import User
from django.contrib import messages
import pyotp
from time import sleep
def log_in(request):
if request.user.is_authenticated:
return redirect('audio:home')
if request.method == "POST":
form = AuthenticationForm(data=request.POST)
if form.is_valid():
user = form.get_user()
if not hasattr(user, 'account'):
account = Account(user=user)
account.save()
user.account = account
if user.account.use_totp:
request.session['user_id'] = user.id
request.session['totp_timeout'] = 1
return redirect('accounts:two_factor_input')
else:
login(request, user)
messages.success(request, 'Successfully logged in!', extra_tags="mb-0")
return redirect('audio:home')
else:
form = AuthenticationForm()
return render(request, 'base_form.html', {'form': form})
def two_factor_input(request):
if request.user.is_authenticated:
return redirect('audio:home')
if 'user_id' not in request.session:
return redirect('audio:home')
user = User.objects.get(id=request.session['user_id'])
if request.method == "POST":
form = EnableTotpForm(request.POST, instance=user.account)
if form.is_valid():
totp_code = form.cleaned_data['totp_code']
if pyotp.TOTP(user.account.totp_key).verify(int(totp_code), valid_window=5):
login(request, user)
del request.session['user_id']
messages.success(request, 'Successfully logged in!', extra_tags="mb-0")
return redirect('audio:home')
else:
form = EnableTotpForm(instance=user.account)
messages.error(request, 'Wrong Code, try again?', extra_tags="mb-0")
sleep(request.session['totp_timeout'])
request.session['totp_timeout'] = request.session['totp_timeout'] * 2
else:
form = EnableTotpForm(instance=user.account)
return render(request, 'accounts/totp_form.html', {'form': form})

View File

@@ -0,0 +1,30 @@
# Generated by Django 3.1.6 on 2021-02-21 22:18
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Account',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('totp_key', models.CharField(max_length=16, null=True)),
('use_totp', models.BooleanField(default=False)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
]

View File

12
accounts/models.py Normal file
View File

@@ -0,0 +1,12 @@
from django.db import models
from tp.models import UUIDAsIDModel
from django.contrib.auth.models import User
class Account(UUIDAsIDModel):
user = models.OneToOneField(User, on_delete=models.CASCADE, unique=True)
totp_key = models.CharField(max_length=16, null=True)
use_totp = models.BooleanField(default=False)
def __str__(self):
return str(self.user)

View File

@@ -0,0 +1,26 @@
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% block content %}
{% include "base_navbar.html" %}
{% include "base_heading.html" %}
<div class="containe mt-4">
<div class="d-flex justify-content-center">
<style> svg { transform: scale(1.5); } </style>
{{ qr | safe }}
</div>
</div>
<div class="container">
<form method="POST" class="d-flex flex-column col-10 offset-1 col-md-2 offset-md-5 mt-5 align-items-center">
{% csrf_token %}
{{ form | crispy }}
<div class="text-center">
<input type="submit" class="btn btn-dark btn-lg mt-5" value="Submit">
</div>
</form>
</div>
{% endblock %}

3
accounts/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

16
accounts/urls.py Normal file
View File

@@ -0,0 +1,16 @@
from django.urls import path
from .enable_totp import enable_totp, disable_totp
from .login import log_in, two_factor_input
from . import views
app_name = "accounts"
urlpatterns = [
path('login/', log_in, name='login'),
path('logout/', views.log_out, name='logout'),
path('edit-profile/', views.edit_profile, name='edit_profile'),
path('password-change/', views.password_change, name='password_change'),
path('enable-totp/', enable_totp, name='enable_totp'),
path('disable-totp/', disable_totp, name='disable_totp'),
path('two-factor-input/', two_factor_input, name='two_factor_input'),
]

48
accounts/views.py Normal file
View File

@@ -0,0 +1,48 @@
from django.shortcuts import render, redirect
from django.contrib.auth.forms import PasswordChangeForm
from django.contrib import messages
from django.contrib.auth import logout, update_session_auth_hash
from .forms import EditProfileForm
def password_change(request):
if not request.user.is_authenticated:
return redirect('audio:home')
if request.method == "POST":
form = PasswordChangeForm(request.user, request.POST)
if form.is_valid():
user = form.save()
update_session_auth_hash(request, user)
messages.success(request, 'Your password was successfully updated!', extra_tags="mb-0")
return redirect('accounts:edit_profile')
else:
form = PasswordChangeForm(request.user)
return render(request, 'base_form.html', {'form': form})
def log_out(request):
if not request.user.is_authenticated:
return redirect('audio:home')
if request.method == "POST":
logout(request)
messages.success(request, 'Successfully Logged Out!', extra_tags="mb-0")
return redirect('audio:home')
return render(request, 'confirmation.html', {})
def edit_profile(request):
if not request.user.is_authenticated:
return redirect('audio:home')
if request.method == "POST":
form = EditProfileForm(request.user, request.POST)
if form.is_valid():
form.save()
messages.success(request, 'Your profile was successfully updated!', extra_tags="mb-0")
return redirect('audio:home')
else:
form = EditProfileForm(request.user, initial={
'email': request.user.email,
'first_name': request.user.first_name,
'last_name': request.user.last_name,
})
return render(request, 'base_form.html', {'form': form})