mirror of
https://expo.survex.com/repositories/troggle/.git
synced 2025-12-14 23:17:05 +00:00
402 lines
19 KiB
Python
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."
|
|
)
|
|
|