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///", 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 " 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." )