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.
69 lines
2.7 KiB
Python
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
|