| 261 | |
| 262 | |
| 263 | async def create_user_without_org( |
| 264 | request: Request, |
| 265 | db_session: AsyncSession, |
| 266 | current_user: PublicUser | AnonymousUser, |
| 267 | user_object: UserCreate, |
| 268 | is_oauth: bool = False, |
| 269 | signup_provider: str = "email", |
| 270 | ): |
| 271 | # Validate password complexity (skip for OAuth users who have empty passwords) |
| 272 | if user_object.password and not is_oauth: |
| 273 | validation_result = validate_password_complexity(user_object.password) |
| 274 | if not validation_result.is_valid: |
| 275 | raise HTTPException( |
| 276 | status_code=400, |
| 277 | detail={ |
| 278 | "code": "WEAK_PASSWORD", |
| 279 | "message": "Password does not meet security requirements", |
| 280 | "errors": validation_result.errors, |
| 281 | "requirements": validation_result.requirements, |
| 282 | }, |
| 283 | ) |
| 284 | |
| 285 | user = User.model_validate(user_object) |
| 286 | |
| 287 | # RBAC check |
| 288 | await rbac_check(request, current_user, "create", "user_x", db_session) |
| 289 | |
| 290 | # Complete the user object |
| 291 | user.user_uuid = f"user_{uuid4()}" |
| 292 | user.password = security_hash_password(user_object.password) if user_object.password else "" |
| 293 | |
| 294 | # OAuth users and OSS mode get auto-verified email |
| 295 | if is_oauth or get_deployment_mode() != 'saas': |
| 296 | user.email_verified = True |
| 297 | user.email_verified_at = datetime.now(timezone.utc).isoformat() |
| 298 | user.signup_method = signup_provider if is_oauth else "email" |
| 299 | else: |
| 300 | user.email_verified = False |
| 301 | user.email_verified_at = None |
| 302 | user.signup_method = "email" |
| 303 | |
| 304 | user.creation_date = str(datetime.now()) |
| 305 | user.update_date = str(datetime.now()) |
| 306 | |
| 307 | # Verifications |
| 308 | |
| 309 | # SECURITY: single generic error for both email and username conflicts to |
| 310 | # prevent account enumeration via this org-less signup endpoint. |
| 311 | conflict = (await db_session.execute( |
| 312 | select(User).where( |
| 313 | (User.username == user.username) | (User.email == user.email) |
| 314 | ) |
| 315 | )).scalars().first() |
| 316 | |
| 317 | if conflict: |
| 318 | raise HTTPException( |
| 319 | status_code=400, |
| 320 | detail="Email or username is already in use", |