2
0
mirror of https://expo.survex.com/repositories/troggle/.git synced 2025-12-14 23:17:05 +00:00
Files
troggle/core/views/user_registration.py

402 lines
19 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,
is_admin_user,
)
from troggle.core.views.auth import expologout
"""
This is the new 2025 individual user login registration, instead of everyone signing
in as "expo". This may be useful for the kanban expo organisation tool. It also will
eventually (?) replace the cookie system for assigning responsibility to git commits.
This registration system has logic spread between these functions and the Django templates.
It seriously needs refactoring as the logic is opaque. Before that, it needs a full set of
tests to check all the combinations of users/logged-on/pre-registered/admin etc. etc.
"""
todo = """
- Make all this work with New people who have never been on expo before - add a queue/email to be approved by an admin
- Stop anyone re-registering an email for an id which already has an email (unless by an admin)
- 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 occurred.
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. Currently allows random anyone to add suff to our system. Yuk.
This is DISABLED pending implementing a queue/confirm mechanism
except for admin users.
"""
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}")
if User.objects.filter(username=nameslug).count() != 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:
if is_admin_user(request.user):
updated_user = register_user(nameslug, email, password=None, pwhash=None, fullname=fullname)
save_users(request, updated_user, email)
return HttpResponseRedirect("/accounts/password_reset/")
else:
return render(request, "login/register.html", {"form": form, "warning": "Only ADMINs can do this", "newuser": True})
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)) # logged_in used on form
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 reshow_disabled(request, url_username, initial_values, warning, admin_notice):
"""Shows the form, but completely disabled, with messages showing what the user did
wrong or inappropriately. Could all be replaced by form validation methods ?
"""
print(warning)
print(admin_notice)
form = register_form(initial=initial_values)
form.fields["username"].widget.attrs["readonly"]="readonly"
form.fields["email"].widget.attrs["readonly"]="readonly"
form.fields["password1"].widget.attrs["readonly"]="readonly"
form.fields["password2"].widget.attrs["readonly"]="readonly"
return render(request, "login/register.html", {"form": form, "admin_notice": admin_notice, "warning": warning})
# not used, loops: return HttpResponse warning, admin_notice): Redirect(f"/accounts/login/?next=/accounts/register/{url_username}")
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.
Authority this gives is the same as the "expo" user
(with cavey:beery password) but specific to an individual.
"""
warning = ""
admin_notice = ""
initial_values={"visible-passwords": "True"}
print(f"{url_username=} {request.user.username=}")
# Since this form is for old expoers, we can expect that they know how to login as 'expo'. So require this at least.
if request.user.is_anonymous:
warning = "ANONYMOUS users not allowed to Register old expoers. Login, e.g. as 'expo', and try again."
return reshow_disabled(request, url_username, initial_values, warning, admin_notice)
if url_username: # if provided in URL, but if POST then this could alternatively be in the POST data field ["username"]
# print(url_username, "Person count",Person.objects.filter(slug=url_username).count())
# print(url_username, "User count",User.objects.filter(username=url_username).count())
if (Person.objects.filter(slug=url_username).count() != 1):
# not an old expoer, so redirect to the other form for completely new cavers,
# but suppose it was a typo? Better to check this in response to a POST surely?
# To do: do this as a form-valiation action.
return HttpResponseRedirect("/accounts/newregister/")
already_registered = (User.objects.filter(username=url_username).count() == 1)
if already_registered:
if is_admin_user(request.user):
admin_notice = "ADMIN OVERRIDE ! Can re-set everything." # can proceed to reset everything
else:
admin_notice = f"This former expoer '{url_username}' already has a registered email address and individual troggle access password."
return reshow_disabled(request, url_username, initial_values, warning, admin_notice)
logged_in = (identified_login := is_identified_user(request.user)) # used on the form
if url_username:
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 a 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, "admin_notice": admin_notice, "warning": warning})
# return HttpResponseRedirect("/accounts/login/")
else: # GET
pass
return render(request, "login/register.html", {"form": form, "admin_notice": admin_notice, "warning": warning})
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[0]} 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."
)