fix(users): require otp verification before google signup

This commit is contained in:
2026-05-14 23:24:09 +03:30
parent 837f5bb49e
commit 4a6f6a08fb

View File

@@ -173,6 +173,32 @@ def _build_claim_required_payload(
} }
def _create_user_and_social_account_from_google_profile(*, mobile: str, profile: GoogleProfile) -> User:
user = User.objects.create_user(
mobile=mobile,
password=None,
first_name=profile.first_name,
last_name=profile.last_name,
email=profile.email,
is_verified=True,
is_active=True,
)
user.set_unusable_password()
user.save(update_fields=["password"])
sync_user_from_google_profile(user, profile)
UserSocialAccount.objects.create(
user=user,
provider=UserSocialAccount.ProviderType.GOOGLE,
provider_user_id=profile.provider_user_id,
email=profile.email,
email_verified=profile.email_verified,
avatar_url=profile.avatar_url,
is_active=True,
)
return user
def _build_public_google_flow_payload(flow_payload: dict[str, Any]) -> dict[str, Any]: def _build_public_google_flow_payload(flow_payload: dict[str, Any]) -> dict[str, Any]:
status = flow_payload.get("status") status = flow_payload.get("status")
if status == "authenticated": if status == "authenticated":
@@ -410,32 +436,62 @@ def complete_google_signup(flow: str, mobile: str) -> dict[str, Any]:
update_google_flow(flow, claim_payload) update_google_flow(flow, claim_payload)
return _build_public_google_flow_payload(claim_payload) return _build_public_google_flow_payload(claim_payload)
user = User.objects.create_user( generate_and_send_otp(normalized_mobile, "register")
mobile=normalized_mobile, signup_payload = {
password=None, "status": "claim_required",
first_name=profile.first_name, "google_profile": _profile_to_payload(profile),
last_name=profile.last_name, "mobile": normalized_mobile,
email=profile.email, "user_id": None,
is_verified=False, "resolution": "new_account",
is_active=True, "email": profile.email,
) "mobile_hint": None,
user.set_unusable_password() "detail": "Verify this mobile number to finish creating your account with Google.",
user.save(update_fields=["password"]) }
sync_user_from_google_profile(user, profile) update_google_flow(flow, signup_payload)
return _build_public_google_flow_payload(signup_payload)
UserSocialAccount.objects.create(
user=user,
provider=UserSocialAccount.ProviderType.GOOGLE,
provider_user_id=profile.provider_user_id,
email=profile.email,
email_verified=profile.email_verified,
avatar_url=profile.avatar_url,
is_active=True,
)
authenticated_payload = build_authenticated_flow_payload(user) def _verify_otp_code(*, mobile: str, code: str) -> None:
update_google_flow(flow, authenticated_payload) from django_redis import get_redis_connection
return authenticated_payload
redis_conn = get_redis_connection("default")
stored_code = redis_conn.get(f"verification_code:{mobile}")
if not stored_code or stored_code.decode("utf-8") != code:
raise ValidationError({"code": "Invalid or expired verification code."})
redis_conn.delete(f"verification_code:{mobile}")
def _create_new_google_user_after_otp(*, mobile: str, profile: GoogleProfile) -> User:
existing_email_user = _find_user_by_email(profile.email)
if existing_email_user is not None:
raise GoogleOAuthFlowError(
"This Google email is already attached to an existing account.",
"google_email_already_claimed",
extra={"mobile_hint": mask_mobile(existing_email_user.mobile)},
)
existing_mobile_user = User.objects.filter(mobile=mobile).first()
if existing_mobile_user is not None:
existing_mobile_email = normalize_email_identity(existing_mobile_user.email)
if existing_mobile_email:
raise GoogleOAuthFlowError(
"This mobile number already belongs to another account with a different email address.",
"google_mobile_belongs_to_other_email",
)
raise GoogleOAuthFlowError(
"This mobile number is no longer available for creating a new Google account.",
"google_flow_invalid_state",
status_code=409,
)
existing_link = find_social_account_for_profile(profile)
if existing_link is not None:
raise GoogleOAuthFlowError(
"This Google account is already attached to another user.",
"google_email_already_claimed",
)
return _create_user_and_social_account_from_google_profile(mobile=mobile, profile=profile)
def send_google_claim_otp(flow: str) -> dict[str, Any]: def send_google_claim_otp(flow: str) -> dict[str, Any]:
@@ -446,13 +502,13 @@ def send_google_claim_otp(flow: str) -> dict[str, Any]:
if not isinstance(mobile, str) or not mobile: if not isinstance(mobile, str) or not mobile:
raise _invalid_flow_error("Claim mobile number is missing.") raise _invalid_flow_error("Claim mobile number is missing.")
generate_and_send_otp(mobile, "login") resolution = flow_payload.get("resolution")
otp_mode = "register" if resolution == "new_account" else "login"
generate_and_send_otp(mobile, otp_mode)
return {"detail": "Verification code sent successfully."} return {"detail": "Verification code sent successfully."}
def verify_google_claim(flow: str, code: str) -> dict[str, Any]: def verify_google_claim(flow: str, code: str) -> dict[str, Any]:
from django_redis import get_redis_connection
flow_payload = get_google_flow_payload(flow) flow_payload = get_google_flow_payload(flow)
_ensure_flow_status(flow_payload, "claim_required") _ensure_flow_status(flow_payload, "claim_required")
@@ -462,18 +518,19 @@ def verify_google_claim(flow: str, code: str) -> dict[str, Any]:
profile = _profile_from_flow(flow_payload) profile = _profile_from_flow(flow_payload)
user_id = flow_payload.get("user_id") user_id = flow_payload.get("user_id")
resolution = flow_payload.get("resolution")
_verify_otp_code(mobile=mobile, code=code)
if resolution == "new_account":
user = _create_new_google_user_after_otp(mobile=mobile, profile=profile)
authenticated_payload = build_authenticated_flow_payload(user)
update_google_flow(flow, authenticated_payload)
return authenticated_payload
user = User.objects.filter(id=user_id, mobile=mobile).first() user = User.objects.filter(id=user_id, mobile=mobile).first()
if not user: if not user:
raise _invalid_flow_error("Target account could not be found.") raise _invalid_flow_error("Target account could not be found.")
redis_conn = get_redis_connection("default")
stored_code = redis_conn.get(f"verification_code:{mobile}")
if not stored_code or stored_code.decode("utf-8") != code:
raise ValidationError({"code": "Invalid or expired verification code."})
redis_conn.delete(f"verification_code:{mobile}")
resolution = flow_payload.get("resolution")
user_email = normalize_email_identity(user.email) user_email = normalize_email_identity(user.email)
if resolution == "existing_email_claim" and user_email != profile.email: if resolution == "existing_email_claim" and user_email != profile.email: