ixg_platform/apps/buyer/forms.py
ixgadmin 19d67555da Add public buyer self-registration
A UK buyer can now create their own account at /portal/signup/. The screen
creates a Keycloak user through a least-privilege service account
(ixg-provisioning: manage-users, view-users) and the matching Django row as
a buyer, then sends them through the normal sign-in flow to the portal.

Staff status is derived from the role in the OIDC backend rather than
asserted, so a self-registered buyer is not staff and does not become staff
on a later sign-in. The platform root and the RBAC middleware now route
each role to its own portal, which also fixes a redirect loop that the
staff fix exposed for non-staff buyers.
2026-09-21 11:30:29 +00:00

69 lines
2.7 KiB
Python

"""The public buyer sign-up form.
This is the only place on the platform a password minimum is enforced — the
realm has no password policy set and no mail service exists — so the rules live
here and are deliberately plain: long enough, not the email or the company name,
and typed twice the same. The form is also the first line of duplicate-email
handling; Keycloak refuses a second account with the same address, and the view
turns that into the same sentence.
"""
from django import forms
from apps.users.models import User
MIN_PASSWORD_LENGTH = 12
DUPLICATE_EMAIL_MESSAGE = "An account with this email already exists — sign in instead."
class BuyerSignupForm(forms.Form):
full_name = forms.CharField(
max_length=255,
label="Full name",
widget=forms.TextInput(attrs={"class": "form-control", "autocomplete": "name"}),
)
company_name = forms.CharField(
max_length=255,
label="Company name",
widget=forms.TextInput(attrs={"class": "form-control", "autocomplete": "organization"}),
)
email = forms.EmailField(
label="Email",
widget=forms.EmailInput(attrs={"class": "form-control", "autocomplete": "email"}),
)
password = forms.CharField(
label="Password",
widget=forms.PasswordInput(attrs={"class": "form-control", "autocomplete": "new-password"}),
help_text=f"At least {MIN_PASSWORD_LENGTH} characters.",
)
confirm_password = forms.CharField(
label="Confirm password",
widget=forms.PasswordInput(attrs={"class": "form-control", "autocomplete": "new-password"}),
)
def clean_email(self):
email = self.cleaned_data["email"].strip().lower()
if User.objects.filter(email__iexact=email).exists():
raise forms.ValidationError(DUPLICATE_EMAIL_MESSAGE)
return email
def clean(self):
cleaned = super().clean()
password = cleaned.get("password")
confirm = cleaned.get("confirm_password")
if password and confirm and password != confirm:
self.add_error("confirm_password", "The two passwords do not match.")
if password:
if len(password) < MIN_PASSWORD_LENGTH:
self.add_error(
"password",
f"Use at least {MIN_PASSWORD_LENGTH} characters.",
)
email = (cleaned.get("email") or "").strip().lower()
company = (cleaned.get("company_name") or "").strip().lower()
if password.lower() in (email, company) and password:
self.add_error(
"password",
"Choose a password that is not your email address or company name.",
)
return cleaned