mirror of
https://github.com/TrentSPalmer/trentpalmerdotorg.git
synced 2025-08-22 21:43:57 -07:00
initial commit
This commit is contained in:
0
accounts/__init__.py
Normal file
0
accounts/__init__.py
Normal file
3
accounts/admin.py
Normal file
3
accounts/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
5
accounts/apps.py
Normal file
5
accounts/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AccountsConfig(AppConfig):
|
||||
name = 'accounts'
|
58
accounts/enable_totp.py
Normal file
58
accounts/enable_totp.py
Normal 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
59
accounts/forms.py
Normal 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
58
accounts/login.py
Normal 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})
|
30
accounts/migrations/0001_initial.py
Normal file
30
accounts/migrations/0001_initial.py
Normal 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,
|
||||
},
|
||||
),
|
||||
]
|
0
accounts/migrations/__init__.py
Normal file
0
accounts/migrations/__init__.py
Normal file
12
accounts/models.py
Normal file
12
accounts/models.py
Normal 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)
|
26
accounts/templates/accounts/totp_form.html
Normal file
26
accounts/templates/accounts/totp_form.html
Normal 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
3
accounts/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
16
accounts/urls.py
Normal file
16
accounts/urls.py
Normal 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
48
accounts/views.py
Normal 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})
|
Reference in New Issue
Block a user