From 1cd2d01e7b9e9f524e77999bdba680e42dd918d2 Mon Sep 17 00:00:00 2001 From: Aviraj <100823015+avirajsingh7@users.noreply.github.com> Date: Mon, 18 Aug 2025 15:00:18 +0530 Subject: [PATCH 1/5] email and password optional in onboarding api request --- backend/app/api/routes/onboarding.py | 29 ++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/backend/app/api/routes/onboarding.py b/backend/app/api/routes/onboarding.py index 12f43dd3d..e2cd70ed5 100644 --- a/backend/app/api/routes/onboarding.py +++ b/backend/app/api/routes/onboarding.py @@ -1,7 +1,8 @@ -import uuid +import re +import secrets from fastapi import APIRouter, HTTPException, Depends -from pydantic import BaseModel, EmailStr +from pydantic import BaseModel, EmailStr, model_validator, field_validator from sqlmodel import Session from app.crud import ( @@ -32,10 +33,30 @@ class OnboardingRequest(BaseModel): organization_name: str project_name: str - email: EmailStr - password: str + email: EmailStr | None = None + password: str | None = None user_name: str + @field_validator("user_name") + def validate_username(cls, v): + pattern = r"^[A-Za-z][A-Za-z0-9._]{2,29}$" + if not re.match(pattern, v): + raise ValueError( + "Username must start with a letter, can contain letters, numbers, underscores, and dots, " + "and must be between 3 and 30 characters long." + ) + return v + + @model_validator(mode="after") + def set_defaults(self): + # Generate email and password if missing + if self.email is None: + self.email = f"{self.user_name}@kaapi.org" + + if self.password is None: + self.password = secrets.token_urlsafe(8) + return self + class OnboardingResponse(BaseModel): organization_id: int From a229095513711e678c29b2f99550d232eab68859 Mon Sep 17 00:00:00 2001 From: Aviraj <100823015+avirajsingh7@users.noreply.github.com> Date: Mon, 18 Aug 2025 15:19:13 +0530 Subject: [PATCH 2/5] add suffix to email autogenerated --- backend/app/api/routes/onboarding.py | 3 ++- backend/app/tests/api/routes/test_onboarding.py | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/backend/app/api/routes/onboarding.py b/backend/app/api/routes/onboarding.py index e2cd70ed5..0b31e8f92 100644 --- a/backend/app/api/routes/onboarding.py +++ b/backend/app/api/routes/onboarding.py @@ -51,7 +51,8 @@ def validate_username(cls, v): def set_defaults(self): # Generate email and password if missing if self.email is None: - self.email = f"{self.user_name}@kaapi.org" + suffix = secrets.token_hex(3) + self.email = f"{self.user_name}.{suffix}@kaapi.org" if self.password is None: self.password = secrets.token_urlsafe(8) diff --git a/backend/app/tests/api/routes/test_onboarding.py b/backend/app/tests/api/routes/test_onboarding.py index 6f0327003..d0f4d85e2 100644 --- a/backend/app/tests/api/routes/test_onboarding.py +++ b/backend/app/tests/api/routes/test_onboarding.py @@ -19,7 +19,7 @@ def test_onboard_user(client, db: Session, superuser_token_headers: dict[str, st "project_name": "TestProject", "email": random_email(), "password": "testpassword123", - "user_name": "Test User", + "user_name": "test_user", } response = client.post( @@ -65,7 +65,7 @@ def test_create_user_existing_email( "project_name": "TestProject", "email": random_email(), "password": "testpassword123", - "user_name": "Test User", + "user_name": "test_user", } client.post( @@ -89,7 +89,7 @@ def test_is_superuser_flag( "project_name": "TestProjects", "email": random_email(), "password": "testpassword123", - "user_name": "Test User", + "user_name": "test_user", } response = client.post( @@ -112,7 +112,7 @@ def test_organization_and_project_creation( "project_name": "NewProject", "email": random_email(), "password": "newpassword123", - "user_name": "New User", + "user_name": "new_user", } response = client.post( From 2f2f09bb51eeb5e2def4588c3270f51c54b28417 Mon Sep 17 00:00:00 2001 From: Aviraj <100823015+avirajsingh7@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:05:13 +0530 Subject: [PATCH 3/5] make username optional --- backend/app/api/routes/onboarding.py | 31 +++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/backend/app/api/routes/onboarding.py b/backend/app/api/routes/onboarding.py index 0b31e8f92..767ed640f 100644 --- a/backend/app/api/routes/onboarding.py +++ b/backend/app/api/routes/onboarding.py @@ -35,27 +35,44 @@ class OnboardingRequest(BaseModel): project_name: str email: EmailStr | None = None password: str | None = None - user_name: str + user_name: str | None = None @field_validator("user_name") def validate_username(cls, v): - pattern = r"^[A-Za-z][A-Za-z0-9._]{2,29}$" + if v is None: + return v + + pattern = r"^[A-Za-z][A-Za-z0-9._]{2,199}$" if not re.match(pattern, v): raise ValueError( "Username must start with a letter, can contain letters, numbers, underscores, and dots, " - "and must be between 3 and 30 characters long." + "and must be between 3 and 200 characters long." ) return v + @staticmethod + def _refactor_username(raw: str, max_len: int = 200) -> str: + """ + Normalize a string into a safe username that can also be used + as the local part of an email address. + """ + username = re.sub(r"[^A-Za-z0-9._]", "_", raw.strip().lower()) + username = re.sub(r"[._]{2,}", "_", username) # collapse repeats + username = username.strip("._") # remove leading/trailing + return username[:max_len] + @model_validator(mode="after") def set_defaults(self): - # Generate email and password if missing + if self.user_name is None: + self.user_name = self._refactor_username(self.project_name) + if self.email is None: + local_part = self._refactor_username(self.user_name, max_len=200) suffix = secrets.token_hex(3) - self.email = f"{self.user_name}.{suffix}@kaapi.org" + self.email = f"{local_part}.{suffix}@kaapi.org" if self.password is None: - self.password = secrets.token_urlsafe(8) + self.password = secrets.token_urlsafe(12) return self @@ -104,7 +121,7 @@ def onboard_user(request: OnboardingRequest, session: SessionDep): user = existing_user else: user_create = UserCreate( - name=request.user_name, + full_name=request.user_name, email=request.email, password=request.password, ) From 77dbd0b0b335143937c6783dcb7f1508d3a771ca Mon Sep 17 00:00:00 2001 From: Aviraj <100823015+avirajsingh7@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:31:54 +0530 Subject: [PATCH 4/5] rename method --- backend/app/api/routes/onboarding.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/app/api/routes/onboarding.py b/backend/app/api/routes/onboarding.py index 767ed640f..c88037083 100644 --- a/backend/app/api/routes/onboarding.py +++ b/backend/app/api/routes/onboarding.py @@ -51,7 +51,7 @@ def validate_username(cls, v): return v @staticmethod - def _refactor_username(raw: str, max_len: int = 200) -> str: + def _clean_username(raw: str, max_len: int = 200) -> str: """ Normalize a string into a safe username that can also be used as the local part of an email address. @@ -64,10 +64,10 @@ def _refactor_username(raw: str, max_len: int = 200) -> str: @model_validator(mode="after") def set_defaults(self): if self.user_name is None: - self.user_name = self._refactor_username(self.project_name) + self.user_name = self._clean_username(self.project_name) if self.email is None: - local_part = self._refactor_username(self.user_name, max_len=200) + local_part = self._clean_username(self.user_name, max_len=200) suffix = secrets.token_hex(3) self.email = f"{local_part}.{suffix}@kaapi.org" From b24059e5c62d48185185d02853670ea349599dd2 Mon Sep 17 00:00:00 2001 From: Aviraj <100823015+avirajsingh7@users.noreply.github.com> Date: Mon, 18 Aug 2025 18:29:18 +0530 Subject: [PATCH 5/5] refactor username --- backend/app/api/routes/onboarding.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/backend/app/api/routes/onboarding.py b/backend/app/api/routes/onboarding.py index c88037083..e2f22dc1d 100644 --- a/backend/app/api/routes/onboarding.py +++ b/backend/app/api/routes/onboarding.py @@ -37,19 +37,6 @@ class OnboardingRequest(BaseModel): password: str | None = None user_name: str | None = None - @field_validator("user_name") - def validate_username(cls, v): - if v is None: - return v - - pattern = r"^[A-Za-z][A-Za-z0-9._]{2,199}$" - if not re.match(pattern, v): - raise ValueError( - "Username must start with a letter, can contain letters, numbers, underscores, and dots, " - "and must be between 3 and 200 characters long." - ) - return v - @staticmethod def _clean_username(raw: str, max_len: int = 200) -> str: """ @@ -64,7 +51,7 @@ def _clean_username(raw: str, max_len: int = 200) -> str: @model_validator(mode="after") def set_defaults(self): if self.user_name is None: - self.user_name = self._clean_username(self.project_name) + self.user_name = self.project_name + " User" if self.email is None: local_part = self._clean_username(self.user_name, max_len=200)