From b522c062c05f8d5f536e7d3b2a146cb68123db34 Mon Sep 17 00:00:00 2001 From: Julia Lange Date: Fri, 29 Aug 2025 13:11:40 -0700 Subject: [PATCH] WIP: Implemented input validation for createAccount --- entryway/src/xrpc/create_account.rs | 82 +++++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 5 deletions(-) diff --git a/entryway/src/xrpc/create_account.rs b/entryway/src/xrpc/create_account.rs index 60d979f..66d5070 100644 --- a/entryway/src/xrpc/create_account.rs +++ b/entryway/src/xrpc/create_account.rs @@ -2,6 +2,8 @@ use router::xrpc::{ProcedureInput, Response, error}; use serde::{Deserialize, Serialize}; use http::status::StatusCode; use tracing::{event, instrument, Level}; +use atproto::types::Handle; +use std::str::FromStr; #[derive(Deserialize, Debug)] pub struct CreateAccountInput { @@ -96,15 +98,85 @@ pub async fn create_account(data: ProcedureInput) -> Respons error(StatusCode::OK, "success", "Account created successfully") } +// Maximum password length (matches atproto TypeScript implementation) +const NEW_PASSWORD_MAX_LENGTH: usize = 256; + // TODO: Implement these helper functions async fn validate_inputs(input: &CreateAccountInput) -> Result { // Based on validateInputsForLocalPds in the TypeScript version - // - Validate email format and not disposable - // - Validate password length - // - Check invite code if required - // - Normalize and validate handle - todo!("Implement input validation") + + // Validate email is provided and has basic format + let email = match &input.email { + Some(e) if !e.is_empty() => e.clone(), + _ => { + return Err(error( + StatusCode::BAD_REQUEST, + "InvalidRequest", + "Email is required" + )); + } + }; + + // Validate email format (basic validation for now) + // TODO: Improve email validation - add proper RFC validation and disposable email checking + // TypeScript version uses @hapi/address for validation and disposable-email-domains-js for disposable check + if !is_valid_email(&email) { + return Err(error( + StatusCode::BAD_REQUEST, + "InvalidRequest", + "This email address is not supported, please use a different email." + )); + } + + // Validate password length if provided + if let Some(password) = &input.password { + if password.len() > NEW_PASSWORD_MAX_LENGTH { + return Err(error( + StatusCode::BAD_REQUEST, + "InvalidRequest", + &format!("Password too long. Maximum length is {} characters.", NEW_PASSWORD_MAX_LENGTH) + )); + } + } + + // Validate and normalize handle using atproto types + let handle = Handle::from_str(&input.handle).map_err(|_| { + error( + StatusCode::BAD_REQUEST, + "InvalidRequest", + "Invalid handle format" + ) + })?; + + // TODO: Invite codes - not supported for now but leave placeholder + if input.invite_code.is_some() { + event!(Level::INFO, "Invite codes not yet supported, ignoring"); + } + + Ok(ValidatedInput { + handle: handle.to_string(), + email: email.to_lowercase(), // Normalize email to lowercase + password: input.password.clone(), + invite_code: input.invite_code.clone(), + }) +} + +// Basic email validation - checks for @ and . in reasonable positions +// TODO: Replace with proper email validation library like email-address crate +fn is_valid_email(email: &str) -> bool { + // Very basic email validation + let at_pos = email.find('@'); + let last_dot_pos = email.rfind('.'); + + match (at_pos, last_dot_pos) { + (Some(at), Some(dot)) => { + // @ must come before the last dot + // Must have content before @, between @ and dot, and after dot + at > 0 && dot > at + 1 && dot < email.len() - 1 + } + _ => false, + } } async fn check_availability(input: &ValidatedInput) -> Result<(), Response> {