diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 8304858..0000000 --- a/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -.env -.pds-data/ -target/ -tmp/ - diff --git a/Cargo.lock b/Cargo.lock index 71ddb9f..ac4e5ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,18 +79,6 @@ 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" @@ -295,15 +283,6 @@ 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" @@ -688,16 +667,11 @@ 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", @@ -1615,17 +1589,6 @@ 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/Cargo.toml b/entryway/Cargo.toml index e4b9f0f..47e68d8 100644 --- a/entryway/Cargo.toml +++ b/entryway/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] -atproto = { workspace = true, features = ["sqlx-support"] } +atproto.workspace = true router.workspace = true http = "1.3.1" serde.workspace = true @@ -12,8 +12,3 @@ 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/01_initial_schema.sql b/entryway/migrations/01_initial_schema.sql deleted file mode 100644 index a6b1ea8..0000000 --- a/entryway/migrations/01_initial_schema.sql +++ /dev/null @@ -1,23 +0,0 @@ --- 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/migrations/20250828184830_initial_schema.sql b/entryway/migrations/20250828184830_initial_schema.sql deleted file mode 100644 index a6b1ea8..0000000 --- a/entryway/migrations/20250828184830_initial_schema.sql +++ /dev/null @@ -1,23 +0,0 @@ --- 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 deleted file mode 100644 index 855e280..0000000 --- a/entryway/src/database/error.rs +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index fc13e9a..0000000 --- a/entryway/src/database/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index b5ec548..0000000 --- a/entryway/src/database/operations.rs +++ /dev/null @@ -1,82 +0,0 @@ -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 98153a4..63c6fc1 100644 --- a/entryway/src/main.rs +++ b/entryway/src/main.rs @@ -1,17 +1,21 @@ use router::{ Router, - xrpc::XrpcEndpoint, + xrpc::{ + XrpcEndpoint, + ProcedureInput, + Response, + error, + }, }; +use serde::Deserialize; 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; +use http::status::StatusCode; +use tracing::{ + event, + instrument, + Level, +}; +use std::fmt::Debug; struct Config { entryway_url: String, @@ -25,36 +29,6 @@ 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()); @@ -62,3 +36,22 @@ 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 deleted file mode 100644 index 7f0a0d6..0000000 --- a/entryway/src/xrpc/create_account.rs +++ /dev/null @@ -1,258 +0,0 @@ -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; -use argon2::{Argon2, PasswordHasher, password_hash::{rand_core::OsRng, SaltString}}; -use time::OffsetDateTime; -use crate::database::{Database, DatabaseError}; - -#[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: 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 { - Ok(input) => input, - Err(err) => return err, - }; - - // 2. Check handle and email availability - // if let Err(err) = check_availability(&database, &validated_input).await { - // return err; - // } - - // 3. Generate DID (placeholder for now) - let did = generate_placeholder_did(&validated_input.handle).await; - - // 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" - ); - } - } - } else { - None - }; - - // 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); - // } - - // 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.clone(), - did: did.clone(), - access_jwt: credentials.access_jwt, - refresh_jwt: credentials.refresh_jwt, - }; - - 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") -} - -// 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 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(database: &Database, input: &ValidatedInput) -> Result<(), Response> { - // Check that handle and email are not already taken - 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_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(".", "-")) -} - -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_account_in_db( - database: &Database, - did: &str, - 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 -} - -#[derive(Debug)] -struct ValidatedInput { - handle: String, - email: String, - password: Option, - invite_code: Option, -} - -#[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 deleted file mode 100644 index 96b9473..0000000 --- a/entryway/src/xrpc/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod create_account; - -pub use create_account::create_account; \ No newline at end of file diff --git a/router/src/lib.rs b/router/src/lib.rs index 788556b..bddad49 100644 --- a/router/src/lib.rs +++ b/router/src/lib.rs @@ -18,21 +18,13 @@ impl Default for Router { Self::new() } } -impl Router -where - S: Clone + Send + Sync + 'static, -{ +impl Router { 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 ff62711..0ed8c36 100644 --- a/router/src/xrpc.rs +++ b/router/src/xrpc.rs @@ -52,11 +52,8 @@ pub fn response(code: StatusCode, message: &str) -> Response { error(code, "", message) } -pub struct QueryInput -where S: Clone + Send + Sync + 'static, -{ +pub struct QueryInput { pub parameters: HashMap, - pub state: S, } impl FromRequestParts for QueryInput where @@ -64,26 +61,23 @@ 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, state }), + Ok(p) => Ok(QueryInput { parameters: p.0 }), Err(e) => Err(error(StatusCode::BAD_REQUEST, "Bad Parameters", &e.body_text())), } } } #[derive(Debug)] -pub struct ProcedureInput -where S: Clone + Send + Sync + 'static, -{ +pub struct ProcedureInput { 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, @@ -101,7 +95,7 @@ where .map(|Json(v)| v) .map_err(|e| error(StatusCode::BAD_REQUEST, "Bad Parameters", &e.body_text()))?; - Ok(ProcedureInput { parameters, input, state }) + Ok(ProcedureInput { parameters, input }) } } @@ -131,14 +125,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, state: &S | -> Response { - match QueryInput::from_request_parts(&mut parts, state).await { + resolver: get(async move | mut parts: Parts | -> Response { + match QueryInput::from_request_parts(&mut parts, &()).await { Ok(qi) => query.call(qi).await, Err(e) => e } @@ -146,15 +140,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, state: &S | -> Response { - match ProcedureInput::::from_request(req, &state).await { + resolver: post(async move | req: Request | -> Response { + match ProcedureInput::::from_request(req, &()).await { Ok(pi) => procedure.call(pi).await, Err(e) => e }