rsnarsna commited on
Commit ·
eea4090
1
Parent(s): 0f3bc3b
Decouple organization creation from registration
Browse files- apps/accounts/forms.py +0 -34
- apps/accounts/services/auth_service.py +3 -13
- apps/accounts/views.py +0 -2
- config/settings/__init__.py +5 -5
- docker-compose.yml +1 -1
- start_dev.ps1 +2 -2
- templates/pages/accounts/register.html +0 -27
apps/accounts/forms.py
CHANGED
|
@@ -45,46 +45,12 @@ class RegistrationForm(forms.Form):
|
|
| 45 |
}
|
| 46 |
),
|
| 47 |
)
|
| 48 |
-
org_name = forms.CharField(
|
| 49 |
-
max_length=255,
|
| 50 |
-
widget=forms.TextInput(
|
| 51 |
-
attrs={
|
| 52 |
-
"class": "form-input",
|
| 53 |
-
"placeholder": "Acme Corp",
|
| 54 |
-
"id": "register-org-name",
|
| 55 |
-
}
|
| 56 |
-
),
|
| 57 |
-
label="Organization Name",
|
| 58 |
-
)
|
| 59 |
-
business_type = forms.ChoiceField(
|
| 60 |
-
choices=[], # Populated dynamically in __init__
|
| 61 |
-
widget=forms.Select(
|
| 62 |
-
attrs={
|
| 63 |
-
"class": "form-input",
|
| 64 |
-
"id": "register-business-type",
|
| 65 |
-
}
|
| 66 |
-
),
|
| 67 |
-
label="Business Type",
|
| 68 |
-
)
|
| 69 |
terms_agreement = forms.BooleanField(
|
| 70 |
required=True,
|
| 71 |
error_messages={"required": "You must agree to the Terms of Service to register."},
|
| 72 |
widget=forms.CheckboxInput(attrs={"class": "h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600", "id": "register-terms"}),
|
| 73 |
)
|
| 74 |
|
| 75 |
-
def __init__(self, *args, **kwargs):
|
| 76 |
-
super().__init__(*args, **kwargs)
|
| 77 |
-
# Fetch dynamic business types from SystemSetting
|
| 78 |
-
try:
|
| 79 |
-
allowed_types_setting = SystemSetting.objects.get(key="ALLOWED_BUSINESS_TYPES")
|
| 80 |
-
types_list = allowed_types_setting.value
|
| 81 |
-
if not isinstance(types_list, list):
|
| 82 |
-
types_list = settings.DEFAULT_BUSINESS_TYPES
|
| 83 |
-
except SystemSetting.DoesNotExist:
|
| 84 |
-
types_list = settings.DEFAULT_BUSINESS_TYPES
|
| 85 |
-
|
| 86 |
-
self.fields["business_type"].choices = [(bt, bt) for bt in types_list]
|
| 87 |
-
|
| 88 |
def clean_email(self) -> str:
|
| 89 |
email = self.cleaned_data["email"].lower()
|
| 90 |
if User.objects.filter(email__iexact=email).exists():
|
|
|
|
| 45 |
}
|
| 46 |
),
|
| 47 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
terms_agreement = forms.BooleanField(
|
| 49 |
required=True,
|
| 50 |
error_messages={"required": "You must agree to the Terms of Service to register."},
|
| 51 |
widget=forms.CheckboxInput(attrs={"class": "h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600", "id": "register-terms"}),
|
| 52 |
)
|
| 53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
def clean_email(self) -> str:
|
| 55 |
email = self.cleaned_data["email"].lower()
|
| 56 |
if User.objects.filter(email__iexact=email).exists():
|
apps/accounts/services/auth_service.py
CHANGED
|
@@ -51,14 +51,12 @@ class PasswordError(Exception):
|
|
| 51 |
def register_user(
|
| 52 |
email: str,
|
| 53 |
password: str,
|
| 54 |
-
org_name: str,
|
| 55 |
-
business_type: str,
|
| 56 |
terms_accepted: bool = False
|
| 57 |
) -> User:
|
| 58 |
"""
|
| 59 |
-
Create a new user account with all associated records
|
| 60 |
|
| 61 |
-
Creates: User + Credentials + Profile + EmailVerification
|
| 62 |
Returns the new User instance.
|
| 63 |
"""
|
| 64 |
# Check against breached passwords
|
|
@@ -80,20 +78,12 @@ def register_user(
|
|
| 80 |
|
| 81 |
EmailVerification.objects.create(user=user)
|
| 82 |
|
| 83 |
-
# Create the initial Organization for this user
|
| 84 |
-
from apps.organizations.services.org_service import create_org
|
| 85 |
-
create_org(
|
| 86 |
-
name=org_name,
|
| 87 |
-
owner=user,
|
| 88 |
-
vertical_type=business_type
|
| 89 |
-
)
|
| 90 |
-
|
| 91 |
# Log intelligence event
|
| 92 |
from apps.intelligence.services import log_event
|
| 93 |
log_event(
|
| 94 |
event_type="user_registered",
|
| 95 |
actor=user,
|
| 96 |
-
metadata={"email_domain": email.split("@")[-1]
|
| 97 |
)
|
| 98 |
|
| 99 |
# Record Terms Consent
|
|
|
|
| 51 |
def register_user(
|
| 52 |
email: str,
|
| 53 |
password: str,
|
|
|
|
|
|
|
| 54 |
terms_accepted: bool = False
|
| 55 |
) -> User:
|
| 56 |
"""
|
| 57 |
+
Create a new user account with all associated records.
|
| 58 |
|
| 59 |
+
Creates: User + Credentials + Profile + EmailVerification.
|
| 60 |
Returns the new User instance.
|
| 61 |
"""
|
| 62 |
# Check against breached passwords
|
|
|
|
| 78 |
|
| 79 |
EmailVerification.objects.create(user=user)
|
| 80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
# Log intelligence event
|
| 82 |
from apps.intelligence.services import log_event
|
| 83 |
log_event(
|
| 84 |
event_type="user_registered",
|
| 85 |
actor=user,
|
| 86 |
+
metadata={"email_domain": email.split("@")[-1]}
|
| 87 |
)
|
| 88 |
|
| 89 |
# Record Terms Consent
|
apps/accounts/views.py
CHANGED
|
@@ -101,8 +101,6 @@ def register_view(request):
|
|
| 101 |
user = register_user(
|
| 102 |
email=form.cleaned_data["email"],
|
| 103 |
password=form.cleaned_data["password"],
|
| 104 |
-
org_name=form.cleaned_data["org_name"],
|
| 105 |
-
business_type=form.cleaned_data["business_type"],
|
| 106 |
terms_accepted=form.cleaned_data.get("terms_agreement", False),
|
| 107 |
)
|
| 108 |
# Conditional Verification or Auto-Login
|
|
|
|
| 101 |
user = register_user(
|
| 102 |
email=form.cleaned_data["email"],
|
| 103 |
password=form.cleaned_data["password"],
|
|
|
|
|
|
|
| 104 |
terms_accepted=form.cleaned_data.get("terms_agreement", False),
|
| 105 |
)
|
| 106 |
# Conditional Verification or Auto-Login
|
config/settings/__init__.py
CHANGED
|
@@ -43,7 +43,7 @@ USE_X_FORWARDED_HOST = True
|
|
| 43 |
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
| 44 |
|
| 45 |
# Subdomain Tenancy Settings
|
| 46 |
-
BASE_DOMAIN = env("BASE_DOMAIN", default="localhost:
|
| 47 |
|
| 48 |
# Cookie Domain — share sessions & CSRF across all *.localhost subdomains
|
| 49 |
# Use None in production unless you explicitly need a wild card subdomain cookie.
|
|
@@ -52,9 +52,9 @@ CSRF_COOKIE_DOMAIN = env("CSRF_COOKIE_DOMAIN", default=None)
|
|
| 52 |
|
| 53 |
# Trust all subdomains for CSRF
|
| 54 |
CSRF_TRUSTED_ORIGINS = env.list("CSRF_TRUSTED_ORIGINS", default=[
|
| 55 |
-
"http://*.localhost:
|
| 56 |
-
"http://localhost:
|
| 57 |
-
"http://127.0.0.1:
|
| 58 |
"https://*.hf.space",
|
| 59 |
"https://*.ngrok-free.app",
|
| 60 |
"https://*.ngrok.io",
|
|
@@ -62,7 +62,7 @@ CSRF_TRUSTED_ORIGINS = env.list("CSRF_TRUSTED_ORIGINS", default=[
|
|
| 62 |
])
|
| 63 |
|
| 64 |
# Site URL (Used for email verification links)
|
| 65 |
-
SITE_URL = "http://localhost:
|
| 66 |
|
| 67 |
INTERNAL_IPS = [] # Added for Django Debug Toolbar, if needed.
|
| 68 |
|
|
|
|
| 43 |
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
| 44 |
|
| 45 |
# Subdomain Tenancy Settings
|
| 46 |
+
BASE_DOMAIN = env("BASE_DOMAIN", default="localhost:8000")
|
| 47 |
|
| 48 |
# Cookie Domain — share sessions & CSRF across all *.localhost subdomains
|
| 49 |
# Use None in production unless you explicitly need a wild card subdomain cookie.
|
|
|
|
| 52 |
|
| 53 |
# Trust all subdomains for CSRF
|
| 54 |
CSRF_TRUSTED_ORIGINS = env.list("CSRF_TRUSTED_ORIGINS", default=[
|
| 55 |
+
"http://*.localhost:8000",
|
| 56 |
+
"http://localhost:8000",
|
| 57 |
+
"http://127.0.0.1:8000",
|
| 58 |
"https://*.hf.space",
|
| 59 |
"https://*.ngrok-free.app",
|
| 60 |
"https://*.ngrok.io",
|
|
|
|
| 62 |
])
|
| 63 |
|
| 64 |
# Site URL (Used for email verification links)
|
| 65 |
+
SITE_URL = "http://localhost:8000"
|
| 66 |
|
| 67 |
INTERNAL_IPS = [] # Added for Django Debug Toolbar, if needed.
|
| 68 |
|
docker-compose.yml
CHANGED
|
@@ -19,7 +19,7 @@ services:
|
|
| 19 |
volumes:
|
| 20 |
- .:/app
|
| 21 |
ports:
|
| 22 |
-
- "
|
| 23 |
environment:
|
| 24 |
- DATABASE_URL=postgres://postgres:postgres@db:5432/saas_db
|
| 25 |
- DJANGO_SETTINGS_MODULE=config.settings
|
|
|
|
| 19 |
volumes:
|
| 20 |
- .:/app
|
| 21 |
ports:
|
| 22 |
+
- "8000:7860"
|
| 23 |
environment:
|
| 24 |
- DATABASE_URL=postgres://postgres:postgres@db:5432/saas_db
|
| 25 |
- DJANGO_SETTINGS_MODULE=config.settings
|
start_dev.ps1
CHANGED
|
@@ -45,5 +45,5 @@ Write-Host "Containers are up and running!" -ForegroundColor Green
|
|
| 45 |
|
| 46 |
# 3. Start ngrok
|
| 47 |
Write-Host "`n[3/3] Generating public ngrok URL..." -ForegroundColor Yellow
|
| 48 |
-
Write-Host "Starting ngrok on port
|
| 49 |
-
ngrok http
|
|
|
|
| 45 |
|
| 46 |
# 3. Start ngrok
|
| 47 |
Write-Host "`n[3/3] Generating public ngrok URL..." -ForegroundColor Yellow
|
| 48 |
+
Write-Host "Starting ngrok on port 8000..." -ForegroundColor Green
|
| 49 |
+
ngrok http 8000
|
templates/pages/accounts/register.html
CHANGED
|
@@ -45,34 +45,7 @@
|
|
| 45 |
{% endif %}
|
| 46 |
</div>
|
| 47 |
|
| 48 |
-
<div class="relative mt-6">
|
| 49 |
-
<div class="absolute inset-0 flex items-center"><div class="w-full border-t border-[--border]"></div></div>
|
| 50 |
-
<div class="relative flex justify-center text-sm/6"><span class="bg-[--card] px-6 text-[--muted-foreground] font-semibold uppercase tracking-wider text-xs">Organization Details</span></div>
|
| 51 |
-
</div>
|
| 52 |
|
| 53 |
-
<div>
|
| 54 |
-
<label for="id_org_name" class="block text-sm/6 font-medium text-[--foreground]">Organization Name</label>
|
| 55 |
-
<div class="mt-2">
|
| 56 |
-
<input id="id_org_name" type="text" name="org_name" required placeholder="Acme Corp" class="block w-full rounded-md bg-[--input] px-3 py-1.5 text-base text-[--foreground] outline-1 -outline-offset-1 outline-[--border] placeholder:text-[--muted-foreground] focus:outline-2 focus:-outline-offset-2 focus:outline-[--primary] sm:text-sm/6" />
|
| 57 |
-
</div>
|
| 58 |
-
{% if form.org_name.errors %}
|
| 59 |
-
<p class="mt-1 text-sm text-[--destructive]">{{ form.org_name.errors[0] }}</p>
|
| 60 |
-
{% endif %}
|
| 61 |
-
</div>
|
| 62 |
-
|
| 63 |
-
<div>
|
| 64 |
-
<label for="id_business_type" class="block text-sm/6 font-medium text-[--foreground]">Business Type</label>
|
| 65 |
-
<div class="mt-2">
|
| 66 |
-
<select id="id_business_type" name="business_type" required class="block w-full rounded-md bg-[--input] px-3 py-2 text-base text-[--foreground] outline-1 -outline-offset-1 outline-[--border] focus:outline-2 focus:-outline-offset-2 focus:outline-[--primary] sm:text-sm/6">
|
| 67 |
-
{% for choice_val, choice_label in form.fields.business_type.choices %}
|
| 68 |
-
<option value="{{ choice_val }}">{{ choice_label }}</option>
|
| 69 |
-
{% endfor %}
|
| 70 |
-
</select>
|
| 71 |
-
</div>
|
| 72 |
-
{% if form.business_type.errors %}
|
| 73 |
-
<p class="mt-1 text-sm text-[--destructive]">{{ form.business_type.errors[0] }}</p>
|
| 74 |
-
{% endif %}
|
| 75 |
-
</div>
|
| 76 |
|
| 77 |
<div class="flex items-start mt-6">
|
| 78 |
<div class="flex h-6 items-center">
|
|
|
|
| 45 |
{% endif %}
|
| 46 |
</div>
|
| 47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
<div class="flex items-start mt-6">
|
| 51 |
<div class="flex h-6 items-center">
|