From 7eb0be102e716ae40d4731ff207f61364cbcef4f Mon Sep 17 00:00:00 2001 From: Julia Lange Date: Fri, 29 Aug 2025 13:10:03 -0700 Subject: [PATCH 1/4] WIP: Setup xrpc module and createAccount template --- entryway/src/main.rs | 37 +----- entryway/src/xrpc/create_account.rs | 186 ++++++++++++++++++++++++++++ entryway/src/xrpc/mod.rs | 3 + 3 files changed, 193 insertions(+), 33 deletions(-) create mode 100644 entryway/src/xrpc/create_account.rs create mode 100644 entryway/src/xrpc/mod.rs diff --git a/entryway/src/main.rs b/entryway/src/main.rs index 63c6fc1..73358ae 100644 --- a/entryway/src/main.rs +++ b/entryway/src/main.rs @@ -1,21 +1,11 @@ use router::{ Router, - xrpc::{ - XrpcEndpoint, - ProcedureInput, - Response, - error, - }, + xrpc::XrpcEndpoint, }; -use serde::Deserialize; use atproto::types::Nsid; -use http::status::StatusCode; -use tracing::{ - event, - instrument, - Level, -}; -use std::fmt::Debug; + +mod xrpc; +use xrpc::create_account; struct Config { entryway_url: String, @@ -36,22 +26,3 @@ async fn main() { router.serve().await; } -#[derive(Deserialize, Debug)] -struct CreateAccountInput { - email: Option, - handle: String, - did: Option, - invite_code: Option, - verification_code: Option, - verification_phone: Option, - password: Option, - recovery_key: Option, - plc_op: Option, -} - -#[instrument] -async fn create_account(data: ProcedureInput) -> Response { - event!(Level::INFO, "In create_account"); - - error(StatusCode::OK, "error", "message") -} diff --git a/entryway/src/xrpc/create_account.rs b/entryway/src/xrpc/create_account.rs new file mode 100644 index 0000000..60d979f --- /dev/null +++ b/entryway/src/xrpc/create_account.rs @@ -0,0 +1,186 @@ +use router::xrpc::{ProcedureInput, Response, error}; +use serde::{Deserialize, Serialize}; +use http::status::StatusCode; +use tracing::{event, instrument, Level}; + +#[derive(Deserialize, Debug)] +pub struct CreateAccountInput { + pub email: Option, + pub handle: String, + pub did: Option, + pub invite_code: Option, + pub verification_code: Option, + pub verification_phone: Option, + pub password: Option, + pub recovery_key: Option, + pub plc_op: Option, +} + +#[derive(Serialize, Debug)] +pub struct CreateAccountResponse { + pub handle: String, + pub did: String, + // pub did_doc: Option, // TODO: Define DidDocument type + pub access_jwt: String, + pub refresh_jwt: String, +} + +#[instrument] +pub async fn create_account(data: ProcedureInput) -> 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") +} + +// 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") +} + +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), 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 { + // 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 { + // 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, + invite_code: Option, +} + +#[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, +} \ No newline at end of file diff --git a/entryway/src/xrpc/mod.rs b/entryway/src/xrpc/mod.rs new file mode 100644 index 0000000..96b9473 --- /dev/null +++ b/entryway/src/xrpc/mod.rs @@ -0,0 +1,3 @@ +pub mod create_account; + +pub use create_account::create_account; \ No newline at end of file From b522c062c05f8d5f536e7d3b2a146cb68123db34 Mon Sep 17 00:00:00 2001 From: Julia Lange Date: Fri, 29 Aug 2025 13:11:40 -0700 Subject: [PATCH 2/4] 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> { From 55b583b6e608d1d3a73b9e56788d95d83dfa112e Mon Sep 17 00:00:00 2001 From: Julia Lange Date: Fri, 29 Aug 2025 13:15:35 -0700 Subject: [PATCH 3/4] WIP implement core account creation logic --- entryway/Cargo.toml | 7 +- .../20250828184830_initial_schema.sql | 23 +++ entryway/src/database/error.rs | 13 ++ entryway/src/database/mod.rs | 5 + entryway/src/database/operations.rs | 82 ++++++++ entryway/src/main.rs | 36 ++++ entryway/src/xrpc/create_account.rs | 188 +++++++++--------- 7 files changed, 259 insertions(+), 95 deletions(-) create mode 100644 entryway/migrations/20250828184830_initial_schema.sql create mode 100644 entryway/src/database/error.rs create mode 100644 entryway/src/database/mod.rs create mode 100644 entryway/src/database/operations.rs diff --git a/entryway/Cargo.toml b/entryway/Cargo.toml index 47e68d8..e4b9f0f 100644 --- a/entryway/Cargo.toml +++ b/entryway/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] -atproto.workspace = true +atproto = { workspace = true, features = ["sqlx-support"] } router.workspace = true http = "1.3.1" serde.workspace = true @@ -12,3 +12,8 @@ serde_json.workspace = true tokio.workspace = true tracing-subscriber.workspace = true tracing.workspace = true +async-trait.workspace = true +sqlx.workspace = true +thiserror.workspace = true +argon2 = "0.5" +time = { version = "0.3", features = ["formatting", "macros"] } diff --git a/entryway/migrations/20250828184830_initial_schema.sql b/entryway/migrations/20250828184830_initial_schema.sql new file mode 100644 index 0000000..a6b1ea8 --- /dev/null +++ b/entryway/migrations/20250828184830_initial_schema.sql @@ -0,0 +1,23 @@ +-- PDS Entryway Account Management Schema +-- Minimal schema for account creation and authentication + +-- Actor table - stores public identity information +CREATE TABLE actor ( + did VARCHAR PRIMARY KEY, + handle VARCHAR, + created_at VARCHAR NOT NULL +); + +-- Case-insensitive unique index on handle +CREATE UNIQUE INDEX actor_handle_lower_idx ON actor (LOWER(handle)); + +-- Account table - stores private authentication data +CREATE TABLE account ( + did VARCHAR PRIMARY KEY, + email VARCHAR NOT NULL, + password_scrypt VARCHAR NOT NULL, + email_confirmed_at VARCHAR +); + +-- Case-insensitive unique index on email +CREATE UNIQUE INDEX account_email_lower_idx ON account (LOWER(email)); diff --git a/entryway/src/database/error.rs b/entryway/src/database/error.rs new file mode 100644 index 0000000..855e280 --- /dev/null +++ b/entryway/src/database/error.rs @@ -0,0 +1,13 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum DatabaseError { + #[error("Database connection error: {0}")] + Connection(#[from] sqlx::Error), + + #[error("Handle already taken: {0}")] + HandleTaken(String), + + #[error("Email already taken: {0}")] + EmailTaken(String), +} \ No newline at end of file diff --git a/entryway/src/database/mod.rs b/entryway/src/database/mod.rs new file mode 100644 index 0000000..fc13e9a --- /dev/null +++ b/entryway/src/database/mod.rs @@ -0,0 +1,5 @@ +pub mod error; +pub mod operations; + +pub use error::DatabaseError; +pub use operations::Database; \ No newline at end of file diff --git a/entryway/src/database/operations.rs b/entryway/src/database/operations.rs new file mode 100644 index 0000000..b5ec548 --- /dev/null +++ b/entryway/src/database/operations.rs @@ -0,0 +1,82 @@ +use sqlx::{Pool, Postgres}; +use atproto::types::{ + Handle + Did +}; +use crate::database::DatabaseError; + +pub struct Database { + pool: Pool, +} + +impl Database { + pub fn new(pool: Pool) -> Self { + Self { pool } + } + + // Account availability checking + pub async fn check_handle_available(&self, handle: &Handle) -> Result { + let count = sqlx::query_scalar!( + "SELECT COUNT(*) FROM actor WHERE LOWER(handle) = LOWER($1)", + handle + ) + .fetch_one(&self.pool) + .await?; + + Ok(count.unwrap_or(0) == 0) + } + + pub async fn check_email_available(&self, email: &str) -> Result { + let count = sqlx::query_scalar!( + "SELECT COUNT(*) FROM account WHERE LOWER(email) = LOWER($1)", + email + ) + .fetch_one(&self.pool) + .await?; + + Ok(count.unwrap_or(0) == 0) + } + + // Account creation + pub async fn create_account( + &self, + did: &Did, + handle: &Handle, + email: &str, + password_hash: &str, + created_at: &str, + ) -> Result<(), DatabaseError> { + // Use a transaction to ensure both actor and account records are created together + let mut tx = self.pool.begin().await?; + + // Insert into actor table + sqlx::query!( + r#" + INSERT INTO actor (did, handle, created_at) + VALUES ($1, $2, $3) + "#, + did, + handle, + created_at + ) + .execute(&mut *tx) + .await?; + + // Insert into account table + sqlx::query!( + r#" + INSERT INTO account (did, email, password_scrypt) + VALUES ($1, $2, $3) + "#, + did, + email, + password_hash + ) + .execute(&mut *tx) + .await?; + + tx.commit().await?; + + Ok(()) + } +} diff --git a/entryway/src/main.rs b/entryway/src/main.rs index 73358ae..98153a4 100644 --- a/entryway/src/main.rs +++ b/entryway/src/main.rs @@ -3,9 +3,15 @@ use router::{ xrpc::XrpcEndpoint, }; use atproto::types::Nsid; +use sqlx::{Pool, Postgres}; +use std::env; +use tracing::{event, Level}; mod xrpc; +mod database; + use xrpc::create_account; +use database::Database; struct Config { entryway_url: String, @@ -19,6 +25,36 @@ async fn main() { let subscriber = tracing_subscriber::FmtSubscriber::new(); let _ = tracing::subscriber::set_global_default(subscriber); + // Set up database connection + let database_url = env::var("DATABASE_URL") + .unwrap_or_else(|_| "postgres://localhost/entryway_dev".to_string()); + + event!(Level::INFO, "Connecting to database: {}", database_url); + + let pool = match sqlx::postgres::PgPoolOptions::new() + .max_connections(5) + .connect(&database_url) + .await + { + Ok(pool) => pool, + Err(e) => { + event!(Level::ERROR, "Failed to connect to database: {}", e); + std::process::exit(1); + } + }; + + let database = Database::new(pool); + + // Run migrations + if let Err(e) = database.run_migrations().await { + event!(Level::ERROR, "Failed to run migrations: {}", e); + std::process::exit(1); + } + + event!(Level::INFO, "Database setup complete"); + + // TODO: Wire up database to XRPC handlers + // For now, keeping the existing router setup let mut router = Router::new(); let create_account_nsid: Nsid = "com.atproto.server.createAccount".parse::().expect("valid nsid"); router = router.add_endpoint(XrpcEndpoint::not_implemented()); diff --git a/entryway/src/xrpc/create_account.rs b/entryway/src/xrpc/create_account.rs index 66d5070..7f0a0d6 100644 --- a/entryway/src/xrpc/create_account.rs +++ b/entryway/src/xrpc/create_account.rs @@ -4,6 +4,9 @@ use http::status::StatusCode; use tracing::{event, instrument, Level}; use atproto::types::Handle; use std::str::FromStr; +use argon2::{Argon2, PasswordHasher, password_hash::{rand_core::OsRng, SaltString}}; +use time::OffsetDateTime; +use crate::database::{Database, DatabaseError}; #[derive(Deserialize, Debug)] pub struct CreateAccountInput { @@ -31,7 +34,8 @@ pub struct CreateAccountResponse { pub async fn create_account(data: ProcedureInput) -> Response { event!(Level::INFO, "Creating account for handle: {}", data.input.handle); - // TODO: Implement the following steps based on the TypeScript reference: + // TODO: Get database from context/config + // For now, this won't compile but shows the intended flow // 1. Input validation let validated_input = match validate_inputs(&data.input).await { @@ -40,59 +44,52 @@ pub async fn create_account(data: ProcedureInput) -> Respons }; // 2. Check handle and email availability - if let Err(err) = check_availability(&validated_input).await { - return err; - } + // if let Err(err) = check_availability(&database, &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, - }; + // 3. Generate DID (placeholder for now) + let did = generate_placeholder_did(&validated_input.handle).await; - // 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; + // 4. Hash password if provided + let password_hash = if let Some(password) = &validated_input.password { + match hash_password(password) { + Ok(hash) => Some(hash), + Err(_) => { + return error( + StatusCode::INTERNAL_SERVER_ERROR, + "InternalServerError", + "Failed to hash password" + ); + } } - } - - // 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, + } else { + None }; - // 8. Sequence events (identity, account, commit, sync) - if let Err(err) = sequence_events(&did, &validated_input.handle, &repo_commit).await { - return err; - } + // 5. Create account in database + let created_at = OffsetDateTime::now_utc().format(&time::format_description::well_known::Iso8601::DEFAULT) + .unwrap_or_else(|_| "unknown".to_string()); + + // if let Err(err) = create_account_in_db(&database, &did, &validated_input, password_hash.as_deref(), &created_at).await { + // return convert_db_error_to_response(err); + // } - // 9. Update repo root - if let Err(err) = update_repo_root(&did, &repo_commit).await { - return err; - } + // 6. Generate session tokens (placeholder for now) + let credentials = Credentials { + access_jwt: "placeholder_access_token".to_string(), + refresh_jwt: "placeholder_refresh_token".to_string(), + }; // Return success response let response = CreateAccountResponse { - handle: validated_input.handle, + handle: validated_input.handle.clone(), did: did.clone(), access_jwt: credentials.access_jwt, refresh_jwt: credentials.refresh_jwt, }; - event!(Level::INFO, "Account created successfully for DID: {}", did); + event!(Level::INFO, "Account created successfully for DID: {} with handle: {}", did, validated_input.handle); // TODO: Replace with proper JSON response encoding error(StatusCode::OK, "success", "Account created successfully") @@ -179,54 +176,73 @@ fn is_valid_email(email: &str) -> bool { } } -async fn check_availability(input: &ValidatedInput) -> Result<(), Response> { +async fn check_availability(database: &Database, input: &ValidatedInput) -> Result<(), Response> { // Check that handle and email are not already taken - todo!("Implement availability checking") + match database.check_handle_available(&input.handle).await { + Ok(false) => { + return Err(error( + StatusCode::BAD_REQUEST, + "InvalidRequest", + &format!("Handle already taken: {}", input.handle) + )); + } + Err(err) => { + event!(Level::ERROR, "Database error checking handle availability: {:?}", err); + return Err(error( + StatusCode::INTERNAL_SERVER_ERROR, + "InternalServerError", + "Database error" + )); + } + _ => {} + } + + match database.check_email_available(&input.email).await { + Ok(false) => { + return Err(error( + StatusCode::BAD_REQUEST, + "InvalidRequest", + &format!("Email already taken: {}", input.email) + )); + } + Err(err) => { + event!(Level::ERROR, "Database error checking email availability: {:?}", err); + return Err(error( + StatusCode::INTERNAL_SERVER_ERROR, + "InternalServerError", + "Database error" + )); + } + _ => {} + } + + Ok(()) } -async fn generate_identity(input: &ValidatedInput) -> Result<(String, SigningKey, Option), Response> { - // Generate signing key - // Create DID and PLC operation if not provided - todo!("Implement identity generation") +async fn generate_placeholder_did(handle: &str) -> String { + // TODO: Replace with actual DID generation (did:plc) + // For now, generate a placeholder DID based on handle + format!("did:placeholder:{}", handle.replace(".", "-")) } -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") +fn hash_password(password: &str) -> Result { + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::default(); + let password_hash = argon2.hash_password(password.as_bytes(), &salt)?; + Ok(password_hash.to_string()) } -async fn create_repository(did: &str) -> Result { - // 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, +async fn create_account_in_db( + database: &Database, did: &str, - repo_commit: &RepoCommit, -) -> Result { - // Create account record and initial session - // Generate JWT tokens - todo!("Implement account and session creation") + input: &ValidatedInput, + password_hash: Option<&str>, + created_at: &str, +) -> Result<(), DatabaseError> { + let hash = password_hash.unwrap_or(""); // Empty hash if no password + database.create_account(did, &input.handle, &input.email, hash, created_at).await } -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, @@ -235,22 +251,6 @@ struct ValidatedInput { invite_code: Option, } -#[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, From 3b98eb4a95968a8f5138475daf00f39ab737b741 Mon Sep 17 00:00:00 2001 From: Julia Lange Date: Fri, 29 Aug 2025 16:54:32 -0700 Subject: [PATCH 4/4] WIP for laptop --- .gitignore | 5 +++ Cargo.lock | 37 +++++++++++++++++++++++ entryway/migrations/01_initial_schema.sql | 23 ++++++++++++++ router/src/lib.rs | 10 +++++- router/src/xrpc.rs | 32 ++++++++++++-------- 5 files changed, 93 insertions(+), 14 deletions(-) create mode 100644 .gitignore create mode 100644 entryway/migrations/01_initial_schema.sql diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8304858 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.env +.pds-data/ +target/ +tmp/ + diff --git a/Cargo.lock b/Cargo.lock index ac4e5ac..71ddb9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,6 +79,18 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "async-lock" version = "3.4.0" @@ -283,6 +295,15 @@ dependencies = [ "serde", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -667,11 +688,16 @@ dependencies = [ name = "entryway" version = "0.1.0" dependencies = [ + "argon2", + "async-trait", "atproto", "http 1.3.1", "router", "serde", "serde_json", + "sqlx", + "thiserror 2.0.12", + "time", "tokio", "tracing", "tracing-subscriber", @@ -1589,6 +1615,17 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" diff --git a/entryway/migrations/01_initial_schema.sql b/entryway/migrations/01_initial_schema.sql new file mode 100644 index 0000000..a6b1ea8 --- /dev/null +++ b/entryway/migrations/01_initial_schema.sql @@ -0,0 +1,23 @@ +-- PDS Entryway Account Management Schema +-- Minimal schema for account creation and authentication + +-- Actor table - stores public identity information +CREATE TABLE actor ( + did VARCHAR PRIMARY KEY, + handle VARCHAR, + created_at VARCHAR NOT NULL +); + +-- Case-insensitive unique index on handle +CREATE UNIQUE INDEX actor_handle_lower_idx ON actor (LOWER(handle)); + +-- Account table - stores private authentication data +CREATE TABLE account ( + did VARCHAR PRIMARY KEY, + email VARCHAR NOT NULL, + password_scrypt VARCHAR NOT NULL, + email_confirmed_at VARCHAR +); + +-- Case-insensitive unique index on email +CREATE UNIQUE INDEX account_email_lower_idx ON account (LOWER(email)); diff --git a/router/src/lib.rs b/router/src/lib.rs index bddad49..788556b 100644 --- a/router/src/lib.rs +++ b/router/src/lib.rs @@ -18,13 +18,21 @@ impl Default for Router { Self::new() } } -impl Router { +impl Router +where + S: Clone + Send + Sync + 'static, +{ pub fn new() -> Self { let router = AxumRouter::new(); let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127,0,0,1)), 6702); Router { router, addr } } + pub fn with_state(mut self, state: S) -> Router { + self.router = self.with_state(S); + self + } + pub fn add_endpoint(mut self, endpoint: E) -> Self { self.router = endpoint.add_to_router(self.router); self diff --git a/router/src/xrpc.rs b/router/src/xrpc.rs index 0ed8c36..ff62711 100644 --- a/router/src/xrpc.rs +++ b/router/src/xrpc.rs @@ -52,8 +52,11 @@ pub fn response(code: StatusCode, message: &str) -> Response { error(code, "", message) } -pub struct QueryInput { +pub struct QueryInput +where S: Clone + Send + Sync + 'static, +{ pub parameters: HashMap, + pub state: S, } impl FromRequestParts for QueryInput where @@ -61,23 +64,26 @@ where { type Rejection = Response; - async fn from_request_parts(parts: &mut Parts, _state: &S) + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let query_params: Result>, QueryRejection> = Query::try_from_uri(&parts.uri); match query_params { - Ok(p) => Ok(QueryInput { parameters: p.0 }), + Ok(p) => Ok(QueryInput { parameters: p.0, state }), Err(e) => Err(error(StatusCode::BAD_REQUEST, "Bad Parameters", &e.body_text())), } } } #[derive(Debug)] -pub struct ProcedureInput { +pub struct ProcedureInput +where S: Clone + Send + Sync + 'static, +{ pub parameters: HashMap, pub input: J, + pub state: S, } -impl FromRequest for ProcedureInput +impl FromRequest for ProcedureInput where J: for<'de> serde::Deserialize<'de> + Send + 'static, Bytes: FromRequest, @@ -95,7 +101,7 @@ where .map(|Json(v)| v) .map_err(|e| error(StatusCode::BAD_REQUEST, "Bad Parameters", &e.body_text()))?; - Ok(ProcedureInput { parameters, input }) + Ok(ProcedureInput { parameters, input, state }) } } @@ -125,14 +131,14 @@ where } impl XrpcEndpoint { - pub fn new_query(nsid: Nsid, query: Q) -> Self + pub fn new_query(nsid: Nsid, query: Q) -> Self where Q: XrpcHandler + Clone { XrpcEndpoint { path: Path::Nsid(nsid), - resolver: get(async move | mut parts: Parts | -> Response { - match QueryInput::from_request_parts(&mut parts, &()).await { + resolver: get(async move | mut parts: Parts, state: &S | -> Response { + match QueryInput::from_request_parts(&mut parts, state).await { Ok(qi) => query.call(qi).await, Err(e) => e } @@ -140,15 +146,15 @@ impl XrpcEndpoint { } } - pub fn new_procedure(nsid: Nsid, procedure: P) -> Self + pub fn new_procedure(nsid: Nsid, procedure: P) -> Self where - P: XrpcHandler> + Clone, + P: XrpcHandler> + Clone, J: for<'de> serde::Deserialize<'de> + Send + 'static, { XrpcEndpoint { path: Path::Nsid(nsid), - resolver: post(async move | req: Request | -> Response { - match ProcedureInput::::from_request(req, &()).await { + resolver: post(async move | req: Request, state: &S | -> Response { + match ProcedureInput::::from_request(req, &state).await { Ok(pi) => procedure.call(pi).await, Err(e) => e }