fix(users): require otp verification before google signup
This commit is contained in:
@@ -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]:
|
||||
status = flow_payload.get("status")
|
||||
if status == "authenticated":
|
||||
@@ -410,32 +436,62 @@ def complete_google_signup(flow: str, mobile: str) -> dict[str, Any]:
|
||||
update_google_flow(flow, claim_payload)
|
||||
return _build_public_google_flow_payload(claim_payload)
|
||||
|
||||
user = User.objects.create_user(
|
||||
mobile=normalized_mobile,
|
||||
password=None,
|
||||
first_name=profile.first_name,
|
||||
last_name=profile.last_name,
|
||||
email=profile.email,
|
||||
is_verified=False,
|
||||
is_active=True,
|
||||
)
|
||||
user.set_unusable_password()
|
||||
user.save(update_fields=["password"])
|
||||
sync_user_from_google_profile(user, profile)
|
||||
generate_and_send_otp(normalized_mobile, "register")
|
||||
signup_payload = {
|
||||
"status": "claim_required",
|
||||
"google_profile": _profile_to_payload(profile),
|
||||
"mobile": normalized_mobile,
|
||||
"user_id": None,
|
||||
"resolution": "new_account",
|
||||
"email": profile.email,
|
||||
"mobile_hint": None,
|
||||
"detail": "Verify this mobile number to finish creating your account with Google.",
|
||||
}
|
||||
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,
|
||||
|
||||
def _verify_otp_code(*, mobile: str, code: str) -> None:
|
||||
from django_redis import get_redis_connection
|
||||
|
||||
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)},
|
||||
)
|
||||
|
||||
authenticated_payload = build_authenticated_flow_payload(user)
|
||||
update_google_flow(flow, authenticated_payload)
|
||||
return authenticated_payload
|
||||
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]:
|
||||
@@ -446,13 +502,13 @@ def send_google_claim_otp(flow: str) -> dict[str, Any]:
|
||||
if not isinstance(mobile, str) or not mobile:
|
||||
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."}
|
||||
|
||||
|
||||
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)
|
||||
_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)
|
||||
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()
|
||||
if not user:
|
||||
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)
|
||||
|
||||
if resolution == "existing_email_claim" and user_email != profile.email:
|
||||
|
||||
Reference in New Issue
Block a user