2025-08-29 13:10:03 -07:00
|
|
|
use router::xrpc::{ProcedureInput, Response, error};
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
use http::status::StatusCode;
|
|
|
|
|
use tracing::{event, instrument, Level};
|
2025-08-29 13:11:40 -07:00
|
|
|
use atproto::types::Handle;
|
|
|
|
|
use std::str::FromStr;
|
2025-08-29 13:10:03 -07:00
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
|
pub struct CreateAccountInput {
|
|
|
|
|
pub email: Option<String>,
|
|
|
|
|
pub handle: String,
|
|
|
|
|
pub did: Option<String>,
|
|
|
|
|
pub invite_code: Option<String>,
|
|
|
|
|
pub verification_code: Option<String>,
|
|
|
|
|
pub verification_phone: Option<String>,
|
|
|
|
|
pub password: Option<String>,
|
|
|
|
|
pub recovery_key: Option<String>,
|
|
|
|
|
pub plc_op: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize, Debug)]
|
|
|
|
|
pub struct CreateAccountResponse {
|
|
|
|
|
pub handle: String,
|
|
|
|
|
pub did: String,
|
|
|
|
|
// pub did_doc: Option<DidDocument>, // TODO: Define DidDocument type
|
|
|
|
|
pub access_jwt: String,
|
|
|
|
|
pub refresh_jwt: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[instrument]
|
|
|
|
|
pub async fn create_account(data: ProcedureInput<CreateAccountInput>) -> Response {
|
|
|
|
|
event!(Level::INFO, "Creating account for handle: {}", data.input.handle);
|
|
|
|
|
|
|
|
|
|
// TODO: Implement the following steps based on the TypeScript reference:
|
|
|
|
|
|
|
|
|
|
// 1. Input validation
|
|
|
|
|
let validated_input = match validate_inputs(&data.input).await {
|
|
|
|
|
Ok(input) => input,
|
|
|
|
|
Err(err) => return err,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 2. Check handle and email availability
|
|
|
|
|
if let Err(err) = check_availability(&validated_input).await {
|
|
|
|
|
return err;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. Generate DID and signing key
|
|
|
|
|
let (did, signing_key, plc_op) = match generate_identity(&validated_input).await {
|
|
|
|
|
Ok(identity) => identity,
|
|
|
|
|
Err(err) => return err,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 4. Create actor store entry
|
|
|
|
|
if let Err(err) = create_actor_store(&did, &signing_key).await {
|
|
|
|
|
return err;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 5. Create repository
|
|
|
|
|
let repo_commit = match create_repository(&did).await {
|
|
|
|
|
Ok(commit) => commit,
|
|
|
|
|
Err(err) => return err,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 6. Submit PLC operation (if needed)
|
|
|
|
|
if let Some(op) = plc_op {
|
|
|
|
|
if let Err(err) = submit_plc_operation(&did, &op).await {
|
|
|
|
|
return err;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 7. Create account and session
|
|
|
|
|
let credentials = match create_account_and_session(&validated_input, &did, &repo_commit).await {
|
|
|
|
|
Ok(creds) => creds,
|
|
|
|
|
Err(err) => return err,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 8. Sequence events (identity, account, commit, sync)
|
|
|
|
|
if let Err(err) = sequence_events(&did, &validated_input.handle, &repo_commit).await {
|
|
|
|
|
return err;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 9. Update repo root
|
|
|
|
|
if let Err(err) = update_repo_root(&did, &repo_commit).await {
|
|
|
|
|
return err;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Return success response
|
|
|
|
|
let response = CreateAccountResponse {
|
|
|
|
|
handle: validated_input.handle,
|
|
|
|
|
did: did.clone(),
|
|
|
|
|
access_jwt: credentials.access_jwt,
|
|
|
|
|
refresh_jwt: credentials.refresh_jwt,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
event!(Level::INFO, "Account created successfully for DID: {}", did);
|
|
|
|
|
|
|
|
|
|
// TODO: Replace with proper JSON response encoding
|
|
|
|
|
error(StatusCode::OK, "success", "Account created successfully")
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-29 13:11:40 -07:00
|
|
|
// Maximum password length (matches atproto TypeScript implementation)
|
|
|
|
|
const NEW_PASSWORD_MAX_LENGTH: usize = 256;
|
|
|
|
|
|
2025-08-29 13:10:03 -07:00
|
|
|
// TODO: Implement these helper functions
|
|
|
|
|
|
|
|
|
|
async fn validate_inputs(input: &CreateAccountInput) -> Result<ValidatedInput, Response> {
|
|
|
|
|
// Based on validateInputsForLocalPds in the TypeScript version
|
2025-08-29 13:11:40 -07:00
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
|
}
|
2025-08-29 13:10:03 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn check_availability(input: &ValidatedInput) -> Result<(), Response> {
|
|
|
|
|
// Check that handle and email are not already taken
|
|
|
|
|
todo!("Implement availability checking")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn generate_identity(input: &ValidatedInput) -> Result<(String, SigningKey, Option<PlcOp>), Response> {
|
|
|
|
|
// Generate signing key
|
|
|
|
|
// Create DID and PLC operation if not provided
|
|
|
|
|
todo!("Implement identity generation")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn create_actor_store(did: &str, signing_key: &SigningKey) -> Result<(), Response> {
|
|
|
|
|
// Create actor store entry for the new account
|
|
|
|
|
todo!("Implement actor store creation")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn create_repository(did: &str) -> Result<RepoCommit, Response> {
|
|
|
|
|
// Create empty repository for the account
|
|
|
|
|
todo!("Implement repository creation")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn submit_plc_operation(did: &str, plc_op: &PlcOp) -> Result<(), Response> {
|
|
|
|
|
// Submit PLC operation to register/update DID
|
|
|
|
|
todo!("Implement PLC operation submission")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn create_account_and_session(
|
|
|
|
|
input: &ValidatedInput,
|
|
|
|
|
did: &str,
|
|
|
|
|
repo_commit: &RepoCommit,
|
|
|
|
|
) -> Result<Credentials, Response> {
|
|
|
|
|
// Create account record and initial session
|
|
|
|
|
// Generate JWT tokens
|
|
|
|
|
todo!("Implement account and session creation")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn sequence_events(did: &str, handle: &str, repo_commit: &RepoCommit) -> Result<(), Response> {
|
|
|
|
|
// Sequence identity, account, commit, and sync events
|
|
|
|
|
todo!("Implement event sequencing")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn update_repo_root(did: &str, repo_commit: &RepoCommit) -> Result<(), Response> {
|
|
|
|
|
// Update repository root reference
|
|
|
|
|
todo!("Implement repo root update")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: Define these types based on our implementation needs
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
struct ValidatedInput {
|
|
|
|
|
handle: String,
|
|
|
|
|
email: String,
|
|
|
|
|
password: Option<String>,
|
|
|
|
|
invite_code: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
struct SigningKey {
|
|
|
|
|
// TODO: Define signing key structure
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
struct PlcOp {
|
|
|
|
|
// TODO: Define PLC operation structure
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
struct RepoCommit {
|
|
|
|
|
cid: String,
|
|
|
|
|
rev: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
struct Credentials {
|
|
|
|
|
access_jwt: String,
|
|
|
|
|
refresh_jwt: String,
|
|
|
|
|
}
|