mirror of
https://expo.survex.com/repositories/troggle/.git
synced 2025-12-16 12:27:10 +00:00
365 lines
16 KiB
Python
365 lines
16 KiB
Python
import json
|
|
|
|
from django import forms
|
|
from django.conf import settings
|
|
from django.http import HttpResponseRedirect
|
|
from django.shortcuts import render
|
|
from django.core.exceptions import ValidationError
|
|
from django.contrib.auth.models import User
|
|
from django.contrib.auth.forms import PasswordResetForm
|
|
|
|
from troggle.core.models.troggle import DataIssue, Person
|
|
from troggle.parsers.users import register_user, get_encryptor, ENCRYPTED_DIR, USERS_FILE
|
|
from troggle.parsers.people import troggle_slugify
|
|
from troggle.core.utils import (
|
|
add_commit,
|
|
is_identified_user
|
|
)
|
|
from troggle.core.views.auth import expologout
|
|
|
|
"""
|
|
This is the new individual user login registration, instead of everyone signing
|
|
in as "expo". This will be useful for the kanban expo organisation tool.
|
|
"""
|
|
|
|
todo = """
|
|
- Make all this work with New people who have never been on expo before
|
|
|
|
- login automatically, and redirect to control panel ?
|
|
"""
|
|
|
|
class ExpoPasswordResetForm(PasswordResetForm):
|
|
"""Because we are using the Django-standard django.contrib.auth mechanisms, the way Django wants us to
|
|
modify them is to subclass their stuff and insert our extras in the subclasses. This is completely
|
|
unlike how the rest of troggle works because we avoid Class-based views.
|
|
We avoid them because they are very hard to debug for newcomer programmers who don't know Django:
|
|
the logic is spread out up a tree of preceding ancestor classes.
|
|
|
|
This is where we would override the template so make the form look visually like the rest of troggle
|
|
"""
|
|
def clean_email(self):
|
|
email = self.cleaned_data.get('email')
|
|
# Add custom validation logic etc.
|
|
print(f" * ExpoPasswordResetForm PASSWORD reset email posted '{email=}'")
|
|
# method_list = [attribute for attribute in dir(PasswordResetForm) if callable(getattr(PasswordResetForm, attribute)) and attribute.startswith('__') is False]
|
|
# print(method_list)
|
|
return email
|
|
|
|
def reset_done(request):
|
|
"""This page is called when a password reset has successively occured
|
|
Unfortunately by this point, we do not know the name of the user who initiated the
|
|
password reset, so when we do the git commit of the encrypted users file
|
|
we do not have a name to put to the responsible person. To do that,
|
|
we would have to intercept at the previous step, the url:
|
|
"reset/<uidb64>/<token>/",
|
|
views.PasswordResetConfirmView.as_view(),
|
|
and this class-based view is a lot more complicated to replace or sub-class.
|
|
|
|
Currently we are doing the git commit anonymously.. though I guess we could attempt to
|
|
read the cookie... if it is set.
|
|
"""
|
|
current_user = request.user
|
|
save_users(request, current_user)
|
|
if current_user.is_anonymous:
|
|
# What we expect, for a completely new user
|
|
return HttpResponseRedirect("/accounts/login/?next=/handbook/troggle/training/trogbegin.html")
|
|
else:
|
|
# This would be for someone already looged in "expo" for example
|
|
return HttpResponseRedirect("/handbook/troggle/training/trogbegin.html")
|
|
|
|
def newregister(request, username=None):
|
|
"""To register a COMPLETELY new user on the troggle system,
|
|
WITHOUT any previous expo attendance.
|
|
"""
|
|
current_user = request.user # if not logged in, this is 'AnonymousUser'
|
|
warning = ""
|
|
|
|
if request.method == "POST":
|
|
form = newregister_form(request.POST)
|
|
if form.is_valid():
|
|
fullname = form.cleaned_data["fullname"]
|
|
email = form.cleaned_data["email"]
|
|
|
|
nameslug = troggle_slugify(fullname)
|
|
print(f"NEW user slug {nameslug}")
|
|
|
|
expoers = User.objects.filter(username=nameslug)
|
|
if len(expoers) != 0:
|
|
# Disallow a name which already exists, use the other form.
|
|
return HttpResponseRedirect(f"/accounts/register/{nameslug}")
|
|
# create User in the system and refresh stored encrypted user list and git commit it:
|
|
updated_user = register_user(nameslug, email, password=None, pwhash=None, fullname=fullname)
|
|
save_users(request, updated_user, email)
|
|
return HttpResponseRedirect("/accounts/password_reset/")
|
|
else: # GET
|
|
form = newregister_form(initial={"visible-passwords": "True"})
|
|
|
|
return render(request, "login/register.html", {"form": form, "warning": warning, "newuser": True})
|
|
|
|
|
|
def re_register_email(request):
|
|
"""For a logged-on user:
|
|
- change the email address
|
|
- trigger reset password ( by email token)
|
|
|
|
and we ignore any username specified in the URL of the request.
|
|
"""
|
|
logged_in = (identified_login := is_identified_user(request.user))
|
|
if not logged_in:
|
|
return HttpResponseRedirect("/accounts/login/")
|
|
|
|
u = request.user
|
|
initial_values = {}
|
|
initial_values.update({"username": u.username})
|
|
initial_values.update({"email": u.email})
|
|
|
|
if request.method == "POST":
|
|
form = register_email_form(request.POST) # only username and password
|
|
if form.is_valid():
|
|
print("POST VALID")
|
|
email = form.cleaned_data["email"]
|
|
u.email = email
|
|
u.save()
|
|
save_users(request, u, email)
|
|
return render(request, "login/register_email.html", {"form": form, "confirmed": True})
|
|
else:
|
|
print("POST INVALID")
|
|
return render(request, "login/register_email.html", {"form": form})
|
|
else: # GET
|
|
form = register_email_form(initial=initial_values)
|
|
return render(request, "login/register_email.html", {"form": form})
|
|
|
|
def register(request, url_username=None):
|
|
"""To register an old expoer as a new user on the troggle system,
|
|
for someone who has previously attended expo,
|
|
similar to the "expo" user
|
|
(with cavey:beery password) but specific to an individual.
|
|
|
|
We only allow this to be done ONCE for each user-id.
|
|
|
|
"""
|
|
warning = ""
|
|
initial_values={"visible-passwords": "True"}
|
|
|
|
logged_in = (identified_login := is_identified_user(request.user))
|
|
if logged_in:
|
|
# logged in as a known real person with a USer logon
|
|
return re_register_email(request) # discarding url_username
|
|
|
|
if not request.user.is_anonymous:
|
|
# logged in as expo or expoadmin, so logout invisibly before we do anything
|
|
expologout(request) # returns a response, which we discard
|
|
|
|
if url_username: # if provided in URL
|
|
if Person.objects.filter(slug=url_username).count() != 1:
|
|
# not an old expoer, so redirect to the other form
|
|
print(Person.objects.filter(slug=url_username).count())
|
|
return HttpResponseRedirect("/accounts/newregister/")
|
|
|
|
initial_values.update({"username": url_username})
|
|
form = register_form(initial=initial_values)
|
|
form.fields["username"].widget.attrs["readonly"]="readonly"
|
|
else:
|
|
form = register_form(initial=initial_values)
|
|
|
|
if request.method == "POST":
|
|
form = register_form(request.POST)
|
|
if form.is_valid():
|
|
print("POST VALID") # so now username and email fields are readonly
|
|
un = form.cleaned_data["username"]
|
|
pw= form.cleaned_data["password1"]
|
|
email = form.cleaned_data["email"]
|
|
expoers = User.objects.filter(username=un)
|
|
# if this is LOGONABLE user and we are not logged on
|
|
# NOT just save the data ! Anyone could do that..
|
|
# we are now in a state where password should only be re-set by email token
|
|
# but rather than redirect (off-putting) we just make the password fields read-only
|
|
if len(expoers) > 0:
|
|
form.fields["password1"].widget.attrs["readonly"]="readonly"
|
|
form.fields["password2"].widget.attrs["readonly"]="readonly"
|
|
|
|
# create User in the system and refresh stored encrypted user list and git commit it:
|
|
updated_user = register_user(un, email, password=pw, pwhash=None)
|
|
save_users(request, updated_user, email)
|
|
# to do, login automatically, and redirect to control panel ?
|
|
form.fields["username"].widget.attrs["readonly"]="readonly"
|
|
form.fields["email"].widget.attrs["readonly"]="readonly"
|
|
return render(request, "login/register.html", {"form": form, "email_stored": True})
|
|
# return HttpResponseRedirect("/accounts/login/")
|
|
else: # GET
|
|
pass
|
|
return render(request, "login/register.html", {"form": form})
|
|
|
|
|
|
def save_users(request, updated_user, email="troggle@exposerver.expo"):
|
|
f = get_encryptor()
|
|
ru = []
|
|
|
|
print(f"\n + Saving users, encrypted emails, and password hashes")
|
|
for u in User.objects.all():
|
|
if u.username in ["expo", "expoadmin"]:
|
|
continue
|
|
e_email = f.encrypt(u.email.encode("utf8")).decode()
|
|
ru.append({"username":u.username, "email": e_email, "pwhash": u.password, "encrypted": True})
|
|
# print(u.username, e_email)
|
|
original = f.decrypt(e_email).decode()
|
|
print(f" - {u.username} - {original}")
|
|
|
|
if updated_user.is_anonymous:
|
|
git_string = f"troggle <troggle@exposerver.expo>"
|
|
else:
|
|
git_string = f"{updated_user.username} <{email}>"
|
|
encryptedfile = settings.EXPOWEB / ENCRYPTED_DIR / USERS_FILE
|
|
try:
|
|
print(f"- Rewriting the entire encrypted set of registered users to disc ")
|
|
write_users(ru, encryptedfile, git_string)
|
|
except:
|
|
message = f'! - Users encrypted data saving failed - \n!! Permissions failure ?! on attempting to save file "{encryptedfile}"'
|
|
print(message)
|
|
raise
|
|
return render(request, "errors/generic.html", {"message": message})
|
|
|
|
def write_users(registered_users, encryptedfile, git_string):
|
|
jsondict = { "registered_users": registered_users }
|
|
try:
|
|
with open(encryptedfile, 'w', encoding='utf-8') as json_f:
|
|
json.dump(jsondict, json_f, indent=1)
|
|
except Exception as e:
|
|
print(f" ! Exception dumping json <{e}>")
|
|
raise
|
|
|
|
commit_msg = f"Online (re-)registration of a troggle User"
|
|
try:
|
|
add_commit(encryptedfile, commit_msg, git_string)
|
|
except Exception as e:
|
|
print(f" ! Exception doing git add/commit <{e}>")
|
|
raise
|
|
return True
|
|
|
|
class newregister_form(forms.Form): # not a model-form, just a form-form
|
|
"""This is the form for a new user who has not been on expo before and
|
|
does notalready have a username
|
|
"""
|
|
fullname = forms.CharField(strip=True, required=True,
|
|
label="Forename Surname",
|
|
widget=forms.TextInput(
|
|
attrs={"size": 35, "placeholder": "e.g. Anathema Device",
|
|
"style": "vertical-align: text-top;"}
|
|
))
|
|
email = forms.CharField(strip=True, required=True,
|
|
label="email",
|
|
widget=forms.TextInput(
|
|
attrs={"size": 35, "placeholder": "e.g. anathema@tackle_store.expo",
|
|
"style": "vertical-align: text-top;"}
|
|
))
|
|
|
|
def clean(self):
|
|
cleaned_data = super().clean()
|
|
fullname = cleaned_data.get("fullname")
|
|
email = cleaned_data.get("email")
|
|
|
|
users = User.objects.filter(email=email)
|
|
if len(users) != 0:
|
|
raise ValidationError(
|
|
"Duplicate email address. Another registered user is already using this email address. Email addresses must be unique as that is how we reset forgotten passwords."
|
|
)
|
|
userslug = troggle_slugify(fullname)
|
|
people = Person.objects.filter(slug=userslug)
|
|
if len(people) != 0:
|
|
raise ValidationError(
|
|
"Duplicate name. There is already a username correspondng to this Forename Surname. " +
|
|
"If you have been on expo before, you need to use the other form at expo.survex.com/accounts/register/ ."
|
|
)
|
|
|
|
class register_email_form(forms.Form): # not a model-form, just a form-form
|
|
"""The widgets are being used EVEN THOUGH we are not using form.as_p() to create the
|
|
HTML form"""
|
|
username = forms.CharField(strip=True, required=True,
|
|
label="Username",
|
|
widget=forms.TextInput(
|
|
attrs={"size": 35, "placeholder": "e.g. anathema-device",
|
|
"style": "vertical-align: text-top;", "readonly": "readonly"} # READONLY for when changing email
|
|
))
|
|
email = forms.CharField(strip=True, required=True,
|
|
label="email",
|
|
widget=forms.TextInput(
|
|
attrs={"size": 35, "placeholder": "e.g. anathema@potatohut.expo",
|
|
"style": "vertical-align: text-top;"}
|
|
))
|
|
|
|
def clean(self):
|
|
cleaned_data = super().clean()
|
|
|
|
email = cleaned_data.get("email")
|
|
users = User.objects.filter(email=email)
|
|
print(f"{len(users)=}")
|
|
if len(users) > 1: # allow 0 (change) or 1 (confirm)
|
|
print("ValidationError")
|
|
raise ValidationError(
|
|
"Duplicate email address. Another registered user is already using this email address. Email addresses must be unique as that is how we reset forgotten passwords."
|
|
)
|
|
|
|
class register_form(forms.Form): # not a model-form, just a form-form
|
|
"""The widgets are being used EVEN THOUGH we are not using form.as_p() to create the
|
|
HTML form"""
|
|
username = forms.CharField(strip=True, required=True,
|
|
label="Username",
|
|
widget=forms.TextInput(
|
|
attrs={"size": 35, "placeholder": "e.g. anathema-device",
|
|
"style": "vertical-align: text-top;"}
|
|
))
|
|
email = forms.CharField(strip=True, required=True,
|
|
label="email",
|
|
widget=forms.TextInput(
|
|
attrs={"size": 35, "placeholder": "e.g. anathema@potatohut.expo",
|
|
"style": "vertical-align: text-top;"}
|
|
))
|
|
password1 = forms.CharField(strip=True, required=True,
|
|
label="Troggle password",
|
|
widget=forms.TextInput(
|
|
attrs={"size": 30, "placeholder": "your new login password",
|
|
"style": "vertical-align: text-top;"}
|
|
))
|
|
password2 = forms.CharField(strip=True, required=True,
|
|
label="Re-type your troggle password",
|
|
widget=forms.TextInput(
|
|
attrs={"size": 30, "placeholder": "same as the password above",
|
|
"style": "vertical-align: text-top;"}
|
|
) )
|
|
|
|
def clean(self):
|
|
cleaned_data = super().clean()
|
|
pw1 = cleaned_data.get("password1")
|
|
pw2 = cleaned_data.get("password2")
|
|
|
|
if pw1 != pw2:
|
|
raise ValidationError(
|
|
"Retyped password does not match initial password: please fix this."
|
|
)
|
|
un = cleaned_data.get("username")
|
|
if un in ["expo", "expoadmin"]:
|
|
raise ValidationError(
|
|
"Sorry, please do not try to be clever. This username is hard-coded and you can't edit it here. We were students once, too."
|
|
)
|
|
|
|
expoers = Person.objects.filter(slug=un)
|
|
if len(expoers) == 0:
|
|
raise ValidationError(
|
|
"Sorry, this is the form for people who have already been to expo. Use the New User registration form (link above)."
|
|
)
|
|
if len(expoers) != 1:
|
|
raise ValidationError(
|
|
"Sorry, that troggle identifier has duplicates. Contact a nerd on the Nerd email list, or (better) the Matrix website chat."
|
|
)
|
|
email = cleaned_data.get("email")
|
|
users = User.objects.filter(email=email)
|
|
if len(users) > 1:
|
|
raise ValidationError(
|
|
f"Duplicate email address. Another registered user {users} is already using this email address. Email addresses must be unique as that is how we reset forgotten passwords."
|
|
)
|
|
if len(users) == 1:
|
|
if users[0].username != un:
|
|
raise ValidationError(
|
|
f"Duplicate email address. Another registered user '{users[0]}' is already using this email address. Email addresses must be unique as that is how we reset forgotten passwords."
|
|
)
|
|
|