this repo has no description

Format and split big files into smaller ones

-424
src/api/identity.rs
··· 1 - use axum::{ 2 - extract::{State, Path}, 3 - Json, 4 - response::{IntoResponse, Response}, 5 - http::StatusCode, 6 - }; 7 - use serde::{Deserialize, Serialize}; 8 - use serde_json::json; 9 - use crate::state::AppState; 10 - use sqlx::Row; 11 - use bcrypt::{hash, DEFAULT_COST}; 12 - use tracing::{info, error}; 13 - use jacquard_repo::{mst::Mst, commit::Commit, storage::BlockStore}; 14 - use jacquard::types::{string::Tid, did::Did, integer::LimitedU32}; 15 - use std::sync::Arc; 16 - use k256::SecretKey; 17 - use rand::rngs::OsRng; 18 - use base64::Engine; 19 - use reqwest; 20 - 21 - #[derive(Deserialize)] 22 - pub struct CreateAccountInput { 23 - pub handle: String, 24 - pub email: String, 25 - pub password: String, 26 - #[serde(rename = "inviteCode")] 27 - pub invite_code: Option<String>, 28 - pub did: Option<String>, 29 - } 30 - 31 - #[derive(Serialize)] 32 - #[serde(rename_all = "camelCase")] 33 - pub struct CreateAccountOutput { 34 - pub access_jwt: String, 35 - pub refresh_jwt: String, 36 - pub handle: String, 37 - pub did: String, 38 - } 39 - 40 - pub async fn create_account( 41 - State(state): State<AppState>, 42 - Json(input): Json<CreateAccountInput>, 43 - ) -> Response { 44 - info!("create_account hit: {}", input.handle); 45 - if input.handle.contains('!') || input.handle.contains('@') { 46 - return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidHandle", "message": "Handle contains invalid characters"}))).into_response(); 47 - } 48 - 49 - let did = if let Some(d) = &input.did { 50 - if d.trim().is_empty() { 51 - format!("did:plc:{}", uuid::Uuid::new_v4()) 52 - } else { 53 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 54 - if let Err(e) = verify_did_web(d, &hostname, &input.handle).await { 55 - return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidDid", "message": e}))).into_response(); 56 - } 57 - d.clone() 58 - } 59 - } else { 60 - format!("did:plc:{}", uuid::Uuid::new_v4()) 61 - }; 62 - 63 - let mut tx = match state.db.begin().await { 64 - Ok(tx) => tx, 65 - Err(e) => { 66 - error!("Error starting transaction: {:?}", e); 67 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 68 - } 69 - }; 70 - 71 - let exists_query = sqlx::query("SELECT 1 FROM users WHERE handle = $1") 72 - .bind(&input.handle) 73 - .fetch_optional(&mut *tx) 74 - .await; 75 - 76 - match exists_query { 77 - Ok(Some(_)) => return (StatusCode::BAD_REQUEST, Json(json!({"error": "HandleTaken", "message": "Handle already taken"}))).into_response(), 78 - Err(e) => { 79 - error!("Error checking handle: {:?}", e); 80 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 81 - } 82 - Ok(None) => {} 83 - } 84 - 85 - if let Some(code) = &input.invite_code { 86 - let invite_query = sqlx::query("SELECT available_uses FROM invite_codes WHERE code = $1 FOR UPDATE") 87 - .bind(code) 88 - .fetch_optional(&mut *tx) 89 - .await; 90 - 91 - match invite_query { 92 - Ok(Some(row)) => { 93 - let uses: i32 = row.get("available_uses"); 94 - if uses <= 0 { 95 - return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidInviteCode", "message": "Invite code exhausted"}))).into_response(); 96 - } 97 - 98 - let update_invite = sqlx::query("UPDATE invite_codes SET available_uses = available_uses - 1 WHERE code = $1") 99 - .bind(code) 100 - .execute(&mut *tx) 101 - .await; 102 - 103 - if let Err(e) = update_invite { 104 - error!("Error updating invite code: {:?}", e); 105 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 106 - } 107 - }, 108 - Ok(None) => return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidInviteCode", "message": "Invite code not found"}))).into_response(), 109 - Err(e) => { 110 - error!("Error checking invite code: {:?}", e); 111 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 112 - } 113 - } 114 - } 115 - 116 - let password_hash = match hash(&input.password, DEFAULT_COST) { 117 - Ok(h) => h, 118 - Err(e) => { 119 - error!("Error hashing password: {:?}", e); 120 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 121 - } 122 - }; 123 - 124 - let user_insert = sqlx::query("INSERT INTO users (handle, email, did, password_hash) VALUES ($1, $2, $3, $4) RETURNING id") 125 - .bind(&input.handle) 126 - .bind(&input.email) 127 - .bind(&did) 128 - .bind(&password_hash) 129 - .fetch_one(&mut *tx) 130 - .await; 131 - 132 - let user_id: uuid::Uuid = match user_insert { 133 - Ok(row) => row.get("id"), 134 - Err(e) => { 135 - error!("Error inserting user: {:?}", e); 136 - // TODO: Check for unique constraint violation on email/did specifically 137 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 138 - } 139 - }; 140 - 141 - let secret_key = SecretKey::random(&mut OsRng); 142 - let secret_key_bytes = secret_key.to_bytes(); 143 - 144 - let key_insert = sqlx::query("INSERT INTO user_keys (user_id, key_bytes) VALUES ($1, $2)") 145 - .bind(user_id) 146 - .bind(&secret_key_bytes[..]) 147 - .execute(&mut *tx) 148 - .await; 149 - 150 - if let Err(e) = key_insert { 151 - error!("Error inserting user key: {:?}", e); 152 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 153 - } 154 - 155 - let mst = Mst::new(Arc::new(state.block_store.clone())); 156 - let mst_root = match mst.root().await { 157 - Ok(c) => c, 158 - Err(e) => { 159 - error!("Error creating MST root: {:?}", e); 160 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 161 - } 162 - }; 163 - 164 - let did_obj = match Did::new(&did) { 165 - Ok(d) => d, 166 - Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Invalid DID"}))).into_response(), 167 - }; 168 - 169 - let rev = Tid::now(LimitedU32::MIN); 170 - 171 - let commit = Commit::new_unsigned( 172 - did_obj, 173 - mst_root, 174 - rev, 175 - None 176 - ); 177 - 178 - let commit_bytes = match commit.to_cbor() { 179 - Ok(b) => b, 180 - Err(e) => { 181 - error!("Error serializing genesis commit: {:?}", e); 182 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 183 - } 184 - }; 185 - 186 - let commit_cid = match state.block_store.put(&commit_bytes).await { 187 - Ok(c) => c, 188 - Err(e) => { 189 - error!("Error saving genesis commit: {:?}", e); 190 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 191 - } 192 - }; 193 - 194 - let repo_insert = sqlx::query("INSERT INTO repos (user_id, repo_root_cid) VALUES ($1, $2)") 195 - .bind(user_id) 196 - .bind(commit_cid.to_string()) 197 - .execute(&mut *tx) 198 - .await; 199 - 200 - if let Err(e) = repo_insert { 201 - error!("Error initializing repo: {:?}", e); 202 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 203 - } 204 - 205 - if let Some(code) = &input.invite_code { 206 - let use_insert = sqlx::query("INSERT INTO invite_code_uses (code, used_by_user) VALUES ($1, $2)") 207 - .bind(code) 208 - .bind(user_id) 209 - .execute(&mut *tx) 210 - .await; 211 - 212 - if let Err(e) = use_insert { 213 - error!("Error recording invite usage: {:?}", e); 214 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 215 - } 216 - } 217 - 218 - let access_jwt = crate::auth::create_access_token(&did, &secret_key_bytes[..]).map_err(|e| { 219 - error!("Error creating access token: {:?}", e); 220 - (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response() 221 - }); 222 - let access_jwt = match access_jwt { 223 - Ok(t) => t, 224 - Err(r) => return r, 225 - }; 226 - 227 - let refresh_jwt = crate::auth::create_refresh_token(&did, &secret_key_bytes[..]).map_err(|e| { 228 - error!("Error creating refresh token: {:?}", e); 229 - (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response() 230 - }); 231 - let refresh_jwt = match refresh_jwt { 232 - Ok(t) => t, 233 - Err(r) => return r, 234 - }; 235 - 236 - let session_insert = sqlx::query("INSERT INTO sessions (access_jwt, refresh_jwt, did) VALUES ($1, $2, $3)") 237 - .bind(&access_jwt) 238 - .bind(&refresh_jwt) 239 - .bind(&did) 240 - .execute(&mut *tx) 241 - .await; 242 - 243 - if let Err(e) = session_insert { 244 - error!("Error inserting session: {:?}", e); 245 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 246 - } 247 - 248 - if let Err(e) = tx.commit().await { 249 - error!("Error committing transaction: {:?}", e); 250 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 251 - } 252 - 253 - (StatusCode::OK, Json(CreateAccountOutput { 254 - access_jwt, 255 - refresh_jwt, 256 - handle: input.handle, 257 - did, 258 - })).into_response() 259 - } 260 - 261 - fn get_jwk(key_bytes: &[u8]) -> serde_json::Value { 262 - use k256::elliptic_curve::sec1::ToEncodedPoint; 263 - 264 - let secret_key = SecretKey::from_slice(key_bytes).expect("Invalid key length"); 265 - let public_key = secret_key.public_key(); 266 - let encoded = public_key.to_encoded_point(false); 267 - let x = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(encoded.x().unwrap()); 268 - let y = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(encoded.y().unwrap()); 269 - 270 - json!({ 271 - "kty": "EC", 272 - "crv": "secp256k1", 273 - "x": x, 274 - "y": y 275 - }) 276 - } 277 - 278 - pub async fn well_known_did(State(_state): State<AppState>) -> impl IntoResponse { 279 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 280 - // Kinda for local dev, encode hostname if it contains port 281 - let did = if hostname.contains(':') { 282 - format!("did:web:{}", hostname.replace(':', "%3A")) 283 - } else { 284 - format!("did:web:{}", hostname) 285 - }; 286 - 287 - Json(json!({ 288 - "@context": ["https://www.w3.org/ns/did/v1"], 289 - "id": did, 290 - "service": [{ 291 - "id": "#atproto_pds", 292 - "type": "AtprotoPersonalDataServer", 293 - "serviceEndpoint": format!("https://{}", hostname) 294 - }] 295 - })) 296 - } 297 - 298 - pub async fn user_did_doc( 299 - State(state): State<AppState>, 300 - Path(handle): Path<String>, 301 - ) -> Response { 302 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 303 - 304 - let user = sqlx::query("SELECT id, did FROM users WHERE handle = $1") 305 - .bind(&handle) 306 - .fetch_optional(&state.db) 307 - .await; 308 - 309 - let (user_id, did) = match user { 310 - Ok(Some(row)) => { 311 - let id: uuid::Uuid = row.get("id"); 312 - let d: String = row.get("did"); 313 - (id, d) 314 - }, 315 - Ok(None) => return (StatusCode::NOT_FOUND, Json(json!({"error": "NotFound"}))).into_response(), 316 - Err(e) => { 317 - error!("DB Error: {:?}", e); 318 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response() 319 - }, 320 - }; 321 - 322 - if !did.starts_with("did:web:") { 323 - return (StatusCode::NOT_FOUND, Json(json!({"error": "NotFound", "message": "User is not did:web"}))).into_response(); 324 - } 325 - 326 - let key_row = sqlx::query("SELECT key_bytes FROM user_keys WHERE user_id = $1") 327 - .bind(user_id) 328 - .fetch_optional(&state.db) 329 - .await; 330 - 331 - let key_bytes: Vec<u8> = match key_row { 332 - Ok(Some(row)) => row.get("key_bytes"), 333 - _ => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(), 334 - }; 335 - 336 - let jwk = get_jwk(&key_bytes); 337 - 338 - Json(json!({ 339 - "@context": ["https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/jws-2020/v1"], 340 - "id": did, 341 - "alsoKnownAs": [format!("at://{}", handle)], 342 - "verificationMethod": [{ 343 - "id": format!("{}#atproto", did), 344 - "type": "JsonWebKey2020", 345 - "controller": did, 346 - "publicKeyJwk": jwk 347 - }], 348 - "service": [{ 349 - "id": "#atproto_pds", 350 - "type": "AtprotoPersonalDataServer", 351 - "serviceEndpoint": format!("https://{}", hostname) 352 - }] 353 - })).into_response() 354 - } 355 - 356 - async fn verify_did_web(did: &str, hostname: &str, handle: &str) -> Result<(), String> { 357 - let expected_prefix = if hostname.contains(':') { 358 - format!("did:web:{}", hostname.replace(':', "%3A")) 359 - } else { 360 - format!("did:web:{}", hostname) 361 - }; 362 - 363 - if did.starts_with(&expected_prefix) { 364 - let suffix = &did[expected_prefix.len()..]; 365 - let expected_suffix = format!(":u:{}", handle); 366 - if suffix == expected_suffix { 367 - Ok(()) 368 - } else { 369 - Err(format!("Invalid DID path for this PDS. Expected {}", expected_suffix)) 370 - } 371 - } else { 372 - let parts: Vec<&str> = did.split(':').collect(); 373 - if parts.len() < 3 || parts[0] != "did" || parts[1] != "web" { 374 - return Err("Invalid did:web format".into()); 375 - } 376 - 377 - let domain_segment = parts[2]; 378 - let domain = domain_segment.replace("%3A", ":"); 379 - 380 - let scheme = if domain.starts_with("localhost") || domain.starts_with("127.0.0.1") { 381 - "http" 382 - } else { 383 - "https" 384 - }; 385 - 386 - let url = if parts.len() == 3 { 387 - format!("{}://{}/.well-known/did.json", scheme, domain) 388 - } else { 389 - let path = parts[3..].join("/"); 390 - format!("{}://{}/{}/did.json", scheme, domain, path) 391 - }; 392 - 393 - let client = reqwest::Client::builder() 394 - .timeout(std::time::Duration::from_secs(5)) 395 - .build() 396 - .map_err(|e| format!("Failed to create client: {}", e))?; 397 - 398 - let resp = client.get(&url).send().await 399 - .map_err(|e| format!("Failed to fetch DID doc: {}", e))?; 400 - 401 - if !resp.status().is_success() { 402 - return Err(format!("Failed to fetch DID doc: HTTP {}", resp.status())); 403 - } 404 - 405 - let doc: serde_json::Value = resp.json().await 406 - .map_err(|e| format!("Failed to parse DID doc: {}", e))?; 407 - 408 - let services = doc["service"].as_array() 409 - .ok_or("No services found in DID doc")?; 410 - 411 - let pds_endpoint = format!("https://{}", hostname); 412 - 413 - let has_valid_service = services.iter().any(|s| { 414 - s["type"] == "AtprotoPersonalDataServer" && 415 - s["serviceEndpoint"] == pds_endpoint 416 - }); 417 - 418 - if has_valid_service { 419 - Ok(()) 420 - } else { 421 - Err(format!("DID document does not list this PDS ({}) as AtprotoPersonalDataServer", pds_endpoint)) 422 - } 423 - } 424 - }
···
+355
src/api/identity/account.rs
···
··· 1 + use super::did::verify_did_web; 2 + use crate::state::AppState; 3 + use axum::{ 4 + Json, 5 + extract::State, 6 + http::StatusCode, 7 + response::{IntoResponse, Response}, 8 + }; 9 + use bcrypt::{DEFAULT_COST, hash}; 10 + use jacquard::types::{did::Did, integer::LimitedU32, string::Tid}; 11 + use jacquard_repo::{commit::Commit, mst::Mst, storage::BlockStore}; 12 + use k256::SecretKey; 13 + use rand::rngs::OsRng; 14 + use serde::{Deserialize, Serialize}; 15 + use serde_json::json; 16 + use sqlx::Row; 17 + use std::sync::Arc; 18 + use tracing::{error, info}; 19 + 20 + #[derive(Deserialize)] 21 + pub struct CreateAccountInput { 22 + pub handle: String, 23 + pub email: String, 24 + pub password: String, 25 + #[serde(rename = "inviteCode")] 26 + pub invite_code: Option<String>, 27 + pub did: Option<String>, 28 + } 29 + 30 + #[derive(Serialize)] 31 + #[serde(rename_all = "camelCase")] 32 + pub struct CreateAccountOutput { 33 + pub access_jwt: String, 34 + pub refresh_jwt: String, 35 + pub handle: String, 36 + pub did: String, 37 + } 38 + 39 + pub async fn create_account( 40 + State(state): State<AppState>, 41 + Json(input): Json<CreateAccountInput>, 42 + ) -> Response { 43 + info!("create_account hit: {}", input.handle); 44 + if input.handle.contains('!') || input.handle.contains('@') { 45 + return ( 46 + StatusCode::BAD_REQUEST, 47 + Json( 48 + json!({"error": "InvalidHandle", "message": "Handle contains invalid characters"}), 49 + ), 50 + ) 51 + .into_response(); 52 + } 53 + 54 + let did = if let Some(d) = &input.did { 55 + if d.trim().is_empty() { 56 + format!("did:plc:{}", uuid::Uuid::new_v4()) 57 + } else { 58 + let hostname = 59 + std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 60 + if let Err(e) = verify_did_web(d, &hostname, &input.handle).await { 61 + return ( 62 + StatusCode::BAD_REQUEST, 63 + Json(json!({"error": "InvalidDid", "message": e})), 64 + ) 65 + .into_response(); 66 + } 67 + d.clone() 68 + } 69 + } else { 70 + format!("did:plc:{}", uuid::Uuid::new_v4()) 71 + }; 72 + 73 + let mut tx = match state.db.begin().await { 74 + Ok(tx) => tx, 75 + Err(e) => { 76 + error!("Error starting transaction: {:?}", e); 77 + return ( 78 + StatusCode::INTERNAL_SERVER_ERROR, 79 + Json(json!({"error": "InternalError"})), 80 + ) 81 + .into_response(); 82 + } 83 + }; 84 + 85 + let exists_query = sqlx::query("SELECT 1 FROM users WHERE handle = $1") 86 + .bind(&input.handle) 87 + .fetch_optional(&mut *tx) 88 + .await; 89 + 90 + match exists_query { 91 + Ok(Some(_)) => { 92 + return ( 93 + StatusCode::BAD_REQUEST, 94 + Json(json!({"error": "HandleTaken", "message": "Handle already taken"})), 95 + ) 96 + .into_response(); 97 + } 98 + Err(e) => { 99 + error!("Error checking handle: {:?}", e); 100 + return ( 101 + StatusCode::INTERNAL_SERVER_ERROR, 102 + Json(json!({"error": "InternalError"})), 103 + ) 104 + .into_response(); 105 + } 106 + Ok(None) => {} 107 + } 108 + 109 + if let Some(code) = &input.invite_code { 110 + let invite_query = 111 + sqlx::query("SELECT available_uses FROM invite_codes WHERE code = $1 FOR UPDATE") 112 + .bind(code) 113 + .fetch_optional(&mut *tx) 114 + .await; 115 + 116 + match invite_query { 117 + Ok(Some(row)) => { 118 + let uses: i32 = row.get("available_uses"); 119 + if uses <= 0 { 120 + return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidInviteCode", "message": "Invite code exhausted"}))).into_response(); 121 + } 122 + 123 + let update_invite = sqlx::query( 124 + "UPDATE invite_codes SET available_uses = available_uses - 1 WHERE code = $1", 125 + ) 126 + .bind(code) 127 + .execute(&mut *tx) 128 + .await; 129 + 130 + if let Err(e) = update_invite { 131 + error!("Error updating invite code: {:?}", e); 132 + return ( 133 + StatusCode::INTERNAL_SERVER_ERROR, 134 + Json(json!({"error": "InternalError"})), 135 + ) 136 + .into_response(); 137 + } 138 + } 139 + Ok(None) => { 140 + return ( 141 + StatusCode::BAD_REQUEST, 142 + Json(json!({"error": "InvalidInviteCode", "message": "Invite code not found"})), 143 + ) 144 + .into_response(); 145 + } 146 + Err(e) => { 147 + error!("Error checking invite code: {:?}", e); 148 + return ( 149 + StatusCode::INTERNAL_SERVER_ERROR, 150 + Json(json!({"error": "InternalError"})), 151 + ) 152 + .into_response(); 153 + } 154 + } 155 + } 156 + 157 + let password_hash = match hash(&input.password, DEFAULT_COST) { 158 + Ok(h) => h, 159 + Err(e) => { 160 + error!("Error hashing password: {:?}", e); 161 + return ( 162 + StatusCode::INTERNAL_SERVER_ERROR, 163 + Json(json!({"error": "InternalError"})), 164 + ) 165 + .into_response(); 166 + } 167 + }; 168 + 169 + let user_insert = sqlx::query("INSERT INTO users (handle, email, did, password_hash) VALUES ($1, $2, $3, $4) RETURNING id") 170 + .bind(&input.handle) 171 + .bind(&input.email) 172 + .bind(&did) 173 + .bind(&password_hash) 174 + .fetch_one(&mut *tx) 175 + .await; 176 + 177 + let user_id: uuid::Uuid = match user_insert { 178 + Ok(row) => row.get("id"), 179 + Err(e) => { 180 + error!("Error inserting user: {:?}", e); 181 + // TODO: Check for unique constraint violation on email/did specifically 182 + return ( 183 + StatusCode::INTERNAL_SERVER_ERROR, 184 + Json(json!({"error": "InternalError"})), 185 + ) 186 + .into_response(); 187 + } 188 + }; 189 + 190 + let secret_key = SecretKey::random(&mut OsRng); 191 + let secret_key_bytes = secret_key.to_bytes(); 192 + 193 + let key_insert = sqlx::query("INSERT INTO user_keys (user_id, key_bytes) VALUES ($1, $2)") 194 + .bind(user_id) 195 + .bind(&secret_key_bytes[..]) 196 + .execute(&mut *tx) 197 + .await; 198 + 199 + if let Err(e) = key_insert { 200 + error!("Error inserting user key: {:?}", e); 201 + return ( 202 + StatusCode::INTERNAL_SERVER_ERROR, 203 + Json(json!({"error": "InternalError"})), 204 + ) 205 + .into_response(); 206 + } 207 + 208 + let mst = Mst::new(Arc::new(state.block_store.clone())); 209 + let mst_root = match mst.root().await { 210 + Ok(c) => c, 211 + Err(e) => { 212 + error!("Error creating MST root: {:?}", e); 213 + return ( 214 + StatusCode::INTERNAL_SERVER_ERROR, 215 + Json(json!({"error": "InternalError"})), 216 + ) 217 + .into_response(); 218 + } 219 + }; 220 + 221 + let did_obj = match Did::new(&did) { 222 + Ok(d) => d, 223 + Err(_) => { 224 + return ( 225 + StatusCode::INTERNAL_SERVER_ERROR, 226 + Json(json!({"error": "InternalError", "message": "Invalid DID"})), 227 + ) 228 + .into_response(); 229 + } 230 + }; 231 + 232 + let rev = Tid::now(LimitedU32::MIN); 233 + 234 + let commit = Commit::new_unsigned(did_obj, mst_root, rev, None); 235 + 236 + let commit_bytes = match commit.to_cbor() { 237 + Ok(b) => b, 238 + Err(e) => { 239 + error!("Error serializing genesis commit: {:?}", e); 240 + return ( 241 + StatusCode::INTERNAL_SERVER_ERROR, 242 + Json(json!({"error": "InternalError"})), 243 + ) 244 + .into_response(); 245 + } 246 + }; 247 + 248 + let commit_cid = match state.block_store.put(&commit_bytes).await { 249 + Ok(c) => c, 250 + Err(e) => { 251 + error!("Error saving genesis commit: {:?}", e); 252 + return ( 253 + StatusCode::INTERNAL_SERVER_ERROR, 254 + Json(json!({"error": "InternalError"})), 255 + ) 256 + .into_response(); 257 + } 258 + }; 259 + 260 + let repo_insert = sqlx::query("INSERT INTO repos (user_id, repo_root_cid) VALUES ($1, $2)") 261 + .bind(user_id) 262 + .bind(commit_cid.to_string()) 263 + .execute(&mut *tx) 264 + .await; 265 + 266 + if let Err(e) = repo_insert { 267 + error!("Error initializing repo: {:?}", e); 268 + return ( 269 + StatusCode::INTERNAL_SERVER_ERROR, 270 + Json(json!({"error": "InternalError"})), 271 + ) 272 + .into_response(); 273 + } 274 + 275 + if let Some(code) = &input.invite_code { 276 + let use_insert = 277 + sqlx::query("INSERT INTO invite_code_uses (code, used_by_user) VALUES ($1, $2)") 278 + .bind(code) 279 + .bind(user_id) 280 + .execute(&mut *tx) 281 + .await; 282 + 283 + if let Err(e) = use_insert { 284 + error!("Error recording invite usage: {:?}", e); 285 + return ( 286 + StatusCode::INTERNAL_SERVER_ERROR, 287 + Json(json!({"error": "InternalError"})), 288 + ) 289 + .into_response(); 290 + } 291 + } 292 + 293 + let access_jwt = crate::auth::create_access_token(&did, &secret_key_bytes[..]).map_err(|e| { 294 + error!("Error creating access token: {:?}", e); 295 + ( 296 + StatusCode::INTERNAL_SERVER_ERROR, 297 + Json(json!({"error": "InternalError"})), 298 + ) 299 + .into_response() 300 + }); 301 + let access_jwt = match access_jwt { 302 + Ok(t) => t, 303 + Err(r) => return r, 304 + }; 305 + 306 + let refresh_jwt = crate::auth::create_refresh_token(&did, &secret_key_bytes[..]).map_err(|e| { 307 + error!("Error creating refresh token: {:?}", e); 308 + ( 309 + StatusCode::INTERNAL_SERVER_ERROR, 310 + Json(json!({"error": "InternalError"})), 311 + ) 312 + .into_response() 313 + }); 314 + let refresh_jwt = match refresh_jwt { 315 + Ok(t) => t, 316 + Err(r) => return r, 317 + }; 318 + 319 + let session_insert = 320 + sqlx::query("INSERT INTO sessions (access_jwt, refresh_jwt, did) VALUES ($1, $2, $3)") 321 + .bind(&access_jwt) 322 + .bind(&refresh_jwt) 323 + .bind(&did) 324 + .execute(&mut *tx) 325 + .await; 326 + 327 + if let Err(e) = session_insert { 328 + error!("Error inserting session: {:?}", e); 329 + return ( 330 + StatusCode::INTERNAL_SERVER_ERROR, 331 + Json(json!({"error": "InternalError"})), 332 + ) 333 + .into_response(); 334 + } 335 + 336 + if let Err(e) = tx.commit().await { 337 + error!("Error committing transaction: {:?}", e); 338 + return ( 339 + StatusCode::INTERNAL_SERVER_ERROR, 340 + Json(json!({"error": "InternalError"})), 341 + ) 342 + .into_response(); 343 + } 344 + 345 + ( 346 + StatusCode::OK, 347 + Json(CreateAccountOutput { 348 + access_jwt, 349 + refresh_jwt, 350 + handle: input.handle, 351 + did, 352 + }), 353 + ) 354 + .into_response() 355 + }
+201
src/api/identity/did.rs
···
··· 1 + use crate::state::AppState; 2 + use axum::{ 3 + Json, 4 + extract::{Path, State}, 5 + http::StatusCode, 6 + response::{IntoResponse, Response}, 7 + }; 8 + use base64::Engine; 9 + use k256::SecretKey; 10 + use k256::elliptic_curve::sec1::ToEncodedPoint; 11 + use reqwest; 12 + use serde_json::json; 13 + use sqlx::Row; 14 + use tracing::error; 15 + 16 + pub fn get_jwk(key_bytes: &[u8]) -> serde_json::Value { 17 + let secret_key = SecretKey::from_slice(key_bytes).expect("Invalid key length"); 18 + let public_key = secret_key.public_key(); 19 + let encoded = public_key.to_encoded_point(false); 20 + let x = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(encoded.x().unwrap()); 21 + let y = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(encoded.y().unwrap()); 22 + 23 + json!({ 24 + "kty": "EC", 25 + "crv": "secp256k1", 26 + "x": x, 27 + "y": y 28 + }) 29 + } 30 + 31 + pub async fn well_known_did(State(_state): State<AppState>) -> impl IntoResponse { 32 + let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 33 + // Kinda for local dev, encode hostname if it contains port 34 + let did = if hostname.contains(':') { 35 + format!("did:web:{}", hostname.replace(':', "%3A")) 36 + } else { 37 + format!("did:web:{}", hostname) 38 + }; 39 + 40 + Json(json!({ 41 + "@context": ["https://www.w3.org/ns/did/v1"], 42 + "id": did, 43 + "service": [{ 44 + "id": "#atproto_pds", 45 + "type": "AtprotoPersonalDataServer", 46 + "serviceEndpoint": format!("https://{}", hostname) 47 + }] 48 + })) 49 + } 50 + 51 + pub async fn user_did_doc(State(state): State<AppState>, Path(handle): Path<String>) -> Response { 52 + let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 53 + 54 + let user = sqlx::query("SELECT id, did FROM users WHERE handle = $1") 55 + .bind(&handle) 56 + .fetch_optional(&state.db) 57 + .await; 58 + 59 + let (user_id, did) = match user { 60 + Ok(Some(row)) => { 61 + let id: uuid::Uuid = row.get("id"); 62 + let d: String = row.get("did"); 63 + (id, d) 64 + } 65 + Ok(None) => { 66 + return (StatusCode::NOT_FOUND, Json(json!({"error": "NotFound"}))).into_response(); 67 + } 68 + Err(e) => { 69 + error!("DB Error: {:?}", e); 70 + return ( 71 + StatusCode::INTERNAL_SERVER_ERROR, 72 + Json(json!({"error": "InternalError"})), 73 + ) 74 + .into_response(); 75 + } 76 + }; 77 + 78 + if !did.starts_with("did:web:") { 79 + return ( 80 + StatusCode::NOT_FOUND, 81 + Json(json!({"error": "NotFound", "message": "User is not did:web"})), 82 + ) 83 + .into_response(); 84 + } 85 + 86 + let key_row = sqlx::query("SELECT key_bytes FROM user_keys WHERE user_id = $1") 87 + .bind(user_id) 88 + .fetch_optional(&state.db) 89 + .await; 90 + 91 + let key_bytes: Vec<u8> = match key_row { 92 + Ok(Some(row)) => row.get("key_bytes"), 93 + _ => { 94 + return ( 95 + StatusCode::INTERNAL_SERVER_ERROR, 96 + Json(json!({"error": "InternalError"})), 97 + ) 98 + .into_response(); 99 + } 100 + }; 101 + 102 + let jwk = get_jwk(&key_bytes); 103 + 104 + Json(json!({ 105 + "@context": ["https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/jws-2020/v1"], 106 + "id": did, 107 + "alsoKnownAs": [format!("at://{}", handle)], 108 + "verificationMethod": [{ 109 + "id": format!("{}#atproto", did), 110 + "type": "JsonWebKey2020", 111 + "controller": did, 112 + "publicKeyJwk": jwk 113 + }], 114 + "service": [{ 115 + "id": "#atproto_pds", 116 + "type": "AtprotoPersonalDataServer", 117 + "serviceEndpoint": format!("https://{}", hostname) 118 + }] 119 + })).into_response() 120 + } 121 + 122 + pub async fn verify_did_web(did: &str, hostname: &str, handle: &str) -> Result<(), String> { 123 + let expected_prefix = if hostname.contains(':') { 124 + format!("did:web:{}", hostname.replace(':', "%3A")) 125 + } else { 126 + format!("did:web:{}", hostname) 127 + }; 128 + 129 + if did.starts_with(&expected_prefix) { 130 + let suffix = &did[expected_prefix.len()..]; 131 + let expected_suffix = format!(":u:{}", handle); 132 + if suffix == expected_suffix { 133 + Ok(()) 134 + } else { 135 + Err(format!( 136 + "Invalid DID path for this PDS. Expected {}", 137 + expected_suffix 138 + )) 139 + } 140 + } else { 141 + let parts: Vec<&str> = did.split(':').collect(); 142 + if parts.len() < 3 || parts[0] != "did" || parts[1] != "web" { 143 + return Err("Invalid did:web format".into()); 144 + } 145 + 146 + let domain_segment = parts[2]; 147 + let domain = domain_segment.replace("%3A", ":"); 148 + 149 + let scheme = if domain.starts_with("localhost") || domain.starts_with("127.0.0.1") { 150 + "http" 151 + } else { 152 + "https" 153 + }; 154 + 155 + let url = if parts.len() == 3 { 156 + format!("{}://{}/.well-known/did.json", scheme, domain) 157 + } else { 158 + let path = parts[3..].join("/"); 159 + format!("{}://{}/{}/did.json", scheme, domain, path) 160 + }; 161 + 162 + let client = reqwest::Client::builder() 163 + .timeout(std::time::Duration::from_secs(5)) 164 + .build() 165 + .map_err(|e| format!("Failed to create client: {}", e))?; 166 + 167 + let resp = client 168 + .get(&url) 169 + .send() 170 + .await 171 + .map_err(|e| format!("Failed to fetch DID doc: {}", e))?; 172 + 173 + if !resp.status().is_success() { 174 + return Err(format!("Failed to fetch DID doc: HTTP {}", resp.status())); 175 + } 176 + 177 + let doc: serde_json::Value = resp 178 + .json() 179 + .await 180 + .map_err(|e| format!("Failed to parse DID doc: {}", e))?; 181 + 182 + let services = doc["service"] 183 + .as_array() 184 + .ok_or("No services found in DID doc")?; 185 + 186 + let pds_endpoint = format!("https://{}", hostname); 187 + 188 + let has_valid_service = services.iter().any(|s| { 189 + s["type"] == "AtprotoPersonalDataServer" && s["serviceEndpoint"] == pds_endpoint 190 + }); 191 + 192 + if has_valid_service { 193 + Ok(()) 194 + } else { 195 + Err(format!( 196 + "DID document does not list this PDS ({}) as AtprotoPersonalDataServer", 197 + pds_endpoint 198 + )) 199 + } 200 + } 201 + }
+5
src/api/identity/mod.rs
···
··· 1 + pub mod account; 2 + pub mod did; 3 + 4 + pub use account::create_account; 5 + pub use did::{user_did_doc, well_known_did};
+3 -3
src/api/mod.rs
··· 1 - pub mod server; 2 - pub mod repo; 3 - pub mod proxy; 4 pub mod identity;
··· 1 pub mod identity; 2 + pub mod proxy; 3 + pub mod repo; 4 + pub mod server;
+24 -19
src/api/proxy.rs
··· 1 use axum::{ 2 extract::{Path, Query, State}, 3 http::{HeaderMap, Method, StatusCode}, 4 response::{IntoResponse, Response}, 5 - body::Bytes, 6 }; 7 use reqwest::Client; 8 - use tracing::{info, error}; 9 use std::collections::HashMap; 10 - use crate::state::AppState; 11 - use sqlx::Row; 12 13 pub async fn proxy_handler( 14 State(state): State<AppState>, ··· 18 Query(params): Query<HashMap<String, String>>, 19 body: Bytes, 20 ) -> Response { 21 - 22 - let proxy_header = headers.get("atproto-proxy") 23 .and_then(|h| h.to_str().ok()) 24 .map(|s| s.to_string()); 25 ··· 27 Some(url) => url.clone(), 28 None => match std::env::var("APPVIEW_URL") { 29 Ok(url) => url, 30 - Err(_) => return (StatusCode::BAD_GATEWAY, "No upstream AppView configured").into_response(), 31 }, 32 }; 33 ··· 37 38 let client = Client::new(); 39 40 - let mut request_builder = client 41 - .request(method_verb, &target_url) 42 - .query(&params); 43 44 let mut auth_header_val = headers.get("Authorization").map(|h| h.clone()); 45 ··· 48 if let Ok(token) = auth_val.to_str() { 49 let token = token.replace("Bearer ", ""); 50 if let Ok(did) = crate::auth::get_did_from_token(&token) { 51 - let key_row = sqlx::query("SELECT k.key_bytes FROM user_keys k JOIN users u ON k.user_id = u.id WHERE u.did = $1") 52 .bind(&did) 53 .fetch_optional(&state.db) 54 .await; 55 56 if let Ok(Some(row)) = key_row { 57 let key_bytes: Vec<u8> = row.get("key_bytes"); 58 - if let Ok(new_token) = crate::auth::create_service_token(&did, aud, &method, &key_bytes) { 59 - if let Ok(val) = axum::http::HeaderValue::from_str(&format!("Bearer {}", new_token)) { 60 - auth_header_val = Some(val); 61 - } 62 } 63 } 64 } ··· 86 Ok(b) => b, 87 Err(e) => { 88 error!("Error reading proxy response body: {:?}", e); 89 - return (StatusCode::BAD_GATEWAY, "Error reading upstream response").into_response(); 90 } 91 }; 92 ··· 99 match response_builder.body(axum::body::Body::from(body)) { 100 Ok(r) => r, 101 Err(e) => { 102 - error!("Error building proxy response: {:?}", e); 103 - (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error").into_response() 104 } 105 } 106 - }, 107 Err(e) => { 108 error!("Error sending proxy request: {:?}", e); 109 if e.is_timeout() {
··· 1 + use crate::state::AppState; 2 use axum::{ 3 + body::Bytes, 4 extract::{Path, Query, State}, 5 http::{HeaderMap, Method, StatusCode}, 6 response::{IntoResponse, Response}, 7 }; 8 use reqwest::Client; 9 + use sqlx::Row; 10 use std::collections::HashMap; 11 + use tracing::{error, info}; 12 13 pub async fn proxy_handler( 14 State(state): State<AppState>, ··· 18 Query(params): Query<HashMap<String, String>>, 19 body: Bytes, 20 ) -> Response { 21 + let proxy_header = headers 22 + .get("atproto-proxy") 23 .and_then(|h| h.to_str().ok()) 24 .map(|s| s.to_string()); 25 ··· 27 Some(url) => url.clone(), 28 None => match std::env::var("APPVIEW_URL") { 29 Ok(url) => url, 30 + Err(_) => { 31 + return (StatusCode::BAD_GATEWAY, "No upstream AppView configured").into_response(); 32 + } 33 }, 34 }; 35 ··· 39 40 let client = Client::new(); 41 42 + let mut request_builder = client.request(method_verb, &target_url).query(&params); 43 44 let mut auth_header_val = headers.get("Authorization").map(|h| h.clone()); 45 ··· 48 if let Ok(token) = auth_val.to_str() { 49 let token = token.replace("Bearer ", ""); 50 if let Ok(did) = crate::auth::get_did_from_token(&token) { 51 + let key_row = sqlx::query("SELECT k.key_bytes FROM user_keys k JOIN users u ON k.user_id = u.id WHERE u.did = $1") 52 .bind(&did) 53 .fetch_optional(&state.db) 54 .await; 55 56 if let Ok(Some(row)) = key_row { 57 let key_bytes: Vec<u8> = row.get("key_bytes"); 58 + if let Ok(new_token) = 59 + crate::auth::create_service_token(&did, aud, &method, &key_bytes) 60 + { 61 + if let Ok(val) = 62 + axum::http::HeaderValue::from_str(&format!("Bearer {}", new_token)) 63 + { 64 + auth_header_val = Some(val); 65 + } 66 } 67 } 68 } ··· 90 Ok(b) => b, 91 Err(e) => { 92 error!("Error reading proxy response body: {:?}", e); 93 + return (StatusCode::BAD_GATEWAY, "Error reading upstream response") 94 + .into_response(); 95 } 96 }; 97 ··· 104 match response_builder.body(axum::body::Body::from(body)) { 105 Ok(r) => r, 106 Err(e) => { 107 + error!("Error building proxy response: {:?}", e); 108 + (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error").into_response() 109 } 110 } 111 + } 112 Err(e) => { 113 error!("Error sending proxy request: {:?}", e); 114 if e.is_timeout() {
-889
src/api/repo.rs
··· 1 - use axum::{ 2 - extract::{State, Query}, 3 - Json, 4 - response::{IntoResponse, Response}, 5 - http::StatusCode, 6 - }; 7 - use serde::{Deserialize, Serialize}; 8 - use serde_json::json; 9 - use crate::state::AppState; 10 - use chrono::Utc; 11 - use sqlx::Row; 12 - use cid::Cid; 13 - use std::str::FromStr; 14 - use jacquard_repo::{mst::Mst, commit::Commit, storage::BlockStore}; 15 - use jacquard::types::{string::{Nsid, Tid}, did::Did, integer::LimitedU32}; 16 - use tracing::error; 17 - use std::sync::Arc; 18 - use sha2::{Sha256, Digest}; 19 - use multihash::Multihash; 20 - use axum::body::Bytes; 21 - 22 - #[derive(Deserialize)] 23 - #[allow(dead_code)] 24 - pub struct CreateRecordInput { 25 - pub repo: String, 26 - pub collection: String, 27 - pub rkey: Option<String>, 28 - pub validate: Option<bool>, 29 - pub record: serde_json::Value, 30 - #[serde(rename = "swapCommit")] 31 - pub swap_commit: Option<String>, 32 - } 33 - 34 - #[derive(Serialize)] 35 - #[serde(rename_all = "camelCase")] 36 - pub struct CreateRecordOutput { 37 - pub uri: String, 38 - pub cid: String, 39 - } 40 - 41 - pub async fn create_record( 42 - State(state): State<AppState>, 43 - headers: axum::http::HeaderMap, 44 - Json(input): Json<CreateRecordInput>, 45 - ) -> Response { 46 - let auth_header = headers.get("Authorization"); 47 - if auth_header.is_none() { 48 - return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationRequired"}))).into_response(); 49 - } 50 - let token = auth_header.unwrap().to_str().unwrap_or("").replace("Bearer ", ""); 51 - 52 - let session = sqlx::query( 53 - "SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.access_jwt = $1" 54 - ) 55 - .bind(&token) 56 - .fetch_optional(&state.db) 57 - .await 58 - .unwrap_or(None); 59 - 60 - let (did, key_bytes) = match session { 61 - Some(row) => (row.get::<String, _>("did"), row.get::<Vec<u8>, _>("key_bytes")), 62 - None => return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed"}))).into_response(), 63 - }; 64 - 65 - if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 66 - return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"}))).into_response(); 67 - } 68 - 69 - if input.repo != did { 70 - return (StatusCode::FORBIDDEN, Json(json!({"error": "InvalidRepo", "message": "Repo does not match authenticated user"}))).into_response(); 71 - } 72 - 73 - let user_query = sqlx::query("SELECT id FROM users WHERE did = $1") 74 - .bind(&did) 75 - .fetch_optional(&state.db) 76 - .await; 77 - 78 - let user_id: uuid::Uuid = match user_query { 79 - Ok(Some(row)) => row.get("id"), 80 - _ => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "User not found"}))).into_response(), 81 - }; 82 - 83 - let repo_root_query = sqlx::query("SELECT repo_root_cid FROM repos WHERE user_id = $1") 84 - .bind(user_id) 85 - .fetch_optional(&state.db) 86 - .await; 87 - 88 - let current_root_cid = match repo_root_query { 89 - Ok(Some(row)) => { 90 - let cid_str: String = row.get("repo_root_cid"); 91 - Cid::from_str(&cid_str).ok() 92 - }, 93 - _ => None, 94 - }; 95 - 96 - if current_root_cid.is_none() { 97 - error!("Repo root not found for user {}", did); 98 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Repo root not found"}))).into_response(); 99 - } 100 - let current_root_cid = current_root_cid.unwrap(); 101 - 102 - let commit_bytes = match state.block_store.get(&current_root_cid).await { 103 - Ok(Some(b)) => b, 104 - Ok(None) => { 105 - error!("Commit block not found: {}", current_root_cid); 106 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 107 - }, 108 - Err(e) => { 109 - error!("Failed to load commit block: {:?}", e); 110 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 111 - } 112 - }; 113 - 114 - let commit = match Commit::from_cbor(&commit_bytes) { 115 - Ok(c) => c, 116 - Err(e) => { 117 - error!("Failed to parse commit: {:?}", e); 118 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 119 - } 120 - }; 121 - 122 - let mst_root = commit.data; 123 - let store = Arc::new(state.block_store.clone()); 124 - let mst = Mst::load(store.clone(), mst_root, None); 125 - 126 - let collection_nsid = match input.collection.parse::<Nsid>() { 127 - Ok(n) => n, 128 - Err(_) => return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidCollection"}))).into_response(), 129 - }; 130 - 131 - let rkey = input.rkey.unwrap_or_else(|| { 132 - Utc::now().format("%Y%m%d%H%M%S%f").to_string() 133 - }); 134 - 135 - let mut record_bytes = Vec::new(); 136 - if let Err(e) = serde_ipld_dagcbor::to_writer(&mut record_bytes, &input.record) { 137 - error!("Error serializing record: {:?}", e); 138 - return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidRecord", "message": "Failed to serialize record"}))).into_response(); 139 - } 140 - 141 - let record_cid = match state.block_store.put(&record_bytes).await { 142 - Ok(c) => c, 143 - Err(e) => { 144 - error!("Failed to save record block: {:?}", e); 145 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 146 - } 147 - }; 148 - 149 - let key = format!("{}/{}", collection_nsid, rkey); 150 - if let Err(e) = mst.update(&key, record_cid).await { 151 - error!("Failed to update MST: {:?}", e); 152 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 153 - } 154 - 155 - let new_mst_root = match mst.root().await { 156 - Ok(c) => c, 157 - Err(e) => { 158 - error!("Failed to get new MST root: {:?}", e); 159 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 160 - } 161 - }; 162 - 163 - let did_obj = match Did::new(&did) { 164 - Ok(d) => d, 165 - Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Invalid DID"}))).into_response(), 166 - }; 167 - 168 - let rev = Tid::now(LimitedU32::MIN); 169 - 170 - let new_commit = Commit::new_unsigned( 171 - did_obj, 172 - new_mst_root, 173 - rev, 174 - Some(current_root_cid) 175 - ); 176 - 177 - let new_commit_bytes = match new_commit.to_cbor() { 178 - Ok(b) => b, 179 - Err(e) => { 180 - error!("Failed to serialize new commit: {:?}", e); 181 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 182 - } 183 - }; 184 - 185 - let new_root_cid = match state.block_store.put(&new_commit_bytes).await { 186 - Ok(c) => c, 187 - Err(e) => { 188 - error!("Failed to save new commit: {:?}", e); 189 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 190 - } 191 - }; 192 - 193 - let update_repo = sqlx::query("UPDATE repos SET repo_root_cid = $1 WHERE user_id = $2") 194 - .bind(new_root_cid.to_string()) 195 - .bind(user_id) 196 - .execute(&state.db) 197 - .await; 198 - 199 - if let Err(e) = update_repo { 200 - error!("Failed to update repo root in DB: {:?}", e); 201 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 202 - } 203 - 204 - let record_insert = sqlx::query( 205 - "INSERT INTO records (repo_id, collection, rkey, record_cid) VALUES ($1, $2, $3, $4) 206 - ON CONFLICT (repo_id, collection, rkey) DO UPDATE SET record_cid = $4, created_at = NOW()" 207 - ) 208 - .bind(user_id) 209 - .bind(&input.collection) 210 - .bind(&rkey) 211 - .bind(record_cid.to_string()) 212 - .execute(&state.db) 213 - .await; 214 - 215 - if let Err(e) = record_insert { 216 - error!("Error inserting record index: {:?}", e); 217 - } 218 - 219 - let output = CreateRecordOutput { 220 - uri: format!("at://{}/{}/{}", input.repo, input.collection, rkey), 221 - cid: record_cid.to_string(), 222 - }; 223 - (StatusCode::OK, Json(output)).into_response() 224 - } 225 - 226 - #[derive(Deserialize)] 227 - #[allow(dead_code)] 228 - pub struct PutRecordInput { 229 - pub repo: String, 230 - pub collection: String, 231 - pub rkey: String, 232 - pub validate: Option<bool>, 233 - pub record: serde_json::Value, 234 - #[serde(rename = "swapCommit")] 235 - pub swap_commit: Option<String>, 236 - } 237 - 238 - #[derive(Serialize)] 239 - #[serde(rename_all = "camelCase")] 240 - pub struct PutRecordOutput { 241 - pub uri: String, 242 - pub cid: String, 243 - } 244 - 245 - pub async fn put_record( 246 - State(state): State<AppState>, 247 - headers: axum::http::HeaderMap, 248 - Json(input): Json<PutRecordInput>, 249 - ) -> Response { 250 - let auth_header = headers.get("Authorization"); 251 - if auth_header.is_none() { 252 - return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationRequired"}))).into_response(); 253 - } 254 - let token = auth_header.unwrap().to_str().unwrap_or("").replace("Bearer ", ""); 255 - 256 - let session = sqlx::query( 257 - "SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.access_jwt = $1" 258 - ) 259 - .bind(&token) 260 - .fetch_optional(&state.db) 261 - .await 262 - .unwrap_or(None); 263 - 264 - let (did, key_bytes) = match session { 265 - Some(row) => (row.get::<String, _>("did"), row.get::<Vec<u8>, _>("key_bytes")), 266 - None => return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed"}))).into_response(), 267 - }; 268 - 269 - if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 270 - return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"}))).into_response(); 271 - } 272 - 273 - if input.repo != did { 274 - return (StatusCode::FORBIDDEN, Json(json!({"error": "InvalidRepo", "message": "Repo does not match authenticated user"}))).into_response(); 275 - } 276 - 277 - let user_query = sqlx::query("SELECT id FROM users WHERE did = $1") 278 - .bind(&did) 279 - .fetch_optional(&state.db) 280 - .await; 281 - 282 - let user_id: uuid::Uuid = match user_query { 283 - Ok(Some(row)) => row.get("id"), 284 - _ => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "User not found"}))).into_response(), 285 - }; 286 - 287 - let repo_root_query = sqlx::query("SELECT repo_root_cid FROM repos WHERE user_id = $1") 288 - .bind(user_id) 289 - .fetch_optional(&state.db) 290 - .await; 291 - 292 - let current_root_cid = match repo_root_query { 293 - Ok(Some(row)) => { 294 - let cid_str: String = row.get("repo_root_cid"); 295 - Cid::from_str(&cid_str).ok() 296 - }, 297 - _ => None, 298 - }; 299 - 300 - if current_root_cid.is_none() { 301 - error!("Repo root not found for user {}", did); 302 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Repo root not found"}))).into_response(); 303 - } 304 - let current_root_cid = current_root_cid.unwrap(); 305 - 306 - let commit_bytes = match state.block_store.get(&current_root_cid).await { 307 - Ok(Some(b)) => b, 308 - Ok(None) => { 309 - error!("Commit block not found: {}", current_root_cid); 310 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Commit block not found"}))).into_response(); 311 - }, 312 - Err(e) => { 313 - error!("Failed to load commit block: {:?}", e); 314 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to load commit block"}))).into_response(); 315 - } 316 - }; 317 - 318 - let commit = match Commit::from_cbor(&commit_bytes) { 319 - Ok(c) => c, 320 - Err(e) => { 321 - error!("Failed to parse commit: {:?}", e); 322 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to parse commit"}))).into_response(); 323 - } 324 - }; 325 - 326 - let mst_root = commit.data; 327 - let store = Arc::new(state.block_store.clone()); 328 - let mst = Mst::load(store.clone(), mst_root, None); 329 - 330 - let collection_nsid = match input.collection.parse::<Nsid>() { 331 - Ok(n) => n, 332 - Err(_) => return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidCollection"}))).into_response(), 333 - }; 334 - 335 - let rkey = input.rkey.clone(); 336 - 337 - let mut record_bytes = Vec::new(); 338 - if let Err(e) = serde_ipld_dagcbor::to_writer(&mut record_bytes, &input.record) { 339 - error!("Error serializing record: {:?}", e); 340 - return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidRecord", "message": "Failed to serialize record"}))).into_response(); 341 - } 342 - 343 - let record_cid = match state.block_store.put(&record_bytes).await { 344 - Ok(c) => c, 345 - Err(e) => { 346 - error!("Failed to save record block: {:?}", e); 347 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to save record block"}))).into_response(); 348 - } 349 - }; 350 - 351 - let key = format!("{}/{}", collection_nsid, rkey); 352 - if let Err(e) = mst.update(&key, record_cid).await { 353 - error!("Failed to update MST: {:?}", e); 354 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": format!("Failed to update MST: {:?}", e)}))).into_response(); 355 - } 356 - 357 - let new_mst_root = match mst.root().await { 358 - Ok(c) => c, 359 - Err(e) => { 360 - error!("Failed to get new MST root: {:?}", e); 361 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to get new MST root"}))).into_response(); 362 - } 363 - }; 364 - 365 - let did_obj = match Did::new(&did) { 366 - Ok(d) => d, 367 - Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Invalid DID"}))).into_response(), 368 - }; 369 - 370 - let rev = Tid::now(LimitedU32::MIN); 371 - 372 - let new_commit = Commit::new_unsigned( 373 - did_obj, 374 - new_mst_root, 375 - rev, 376 - Some(current_root_cid) 377 - ); 378 - 379 - let new_commit_bytes = match new_commit.to_cbor() { 380 - Ok(b) => b, 381 - Err(e) => { 382 - error!("Failed to serialize new commit: {:?}", e); 383 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to serialize new commit"}))).into_response(); 384 - } 385 - }; 386 - 387 - let new_root_cid = match state.block_store.put(&new_commit_bytes).await { 388 - Ok(c) => c, 389 - Err(e) => { 390 - error!("Failed to save new commit: {:?}", e); 391 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to save new commit"}))).into_response(); 392 - } 393 - }; 394 - 395 - let update_repo = sqlx::query("UPDATE repos SET repo_root_cid = $1 WHERE user_id = $2") 396 - .bind(new_root_cid.to_string()) 397 - .bind(user_id) 398 - .execute(&state.db) 399 - .await; 400 - 401 - if let Err(e) = update_repo { 402 - error!("Failed to update repo root in DB: {:?}", e); 403 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to update repo root in DB"}))).into_response(); 404 - } 405 - 406 - let record_insert = sqlx::query( 407 - "INSERT INTO records (repo_id, collection, rkey, record_cid) VALUES ($1, $2, $3, $4) 408 - ON CONFLICT (repo_id, collection, rkey) DO UPDATE SET record_cid = $4, created_at = NOW()" 409 - ) 410 - .bind(user_id) 411 - .bind(&input.collection) 412 - .bind(&rkey) 413 - .bind(record_cid.to_string()) 414 - .execute(&state.db) 415 - .await; 416 - 417 - if let Err(e) = record_insert { 418 - error!("Error inserting record index: {:?}", e); 419 - } 420 - 421 - let output = PutRecordOutput { 422 - uri: format!("at://{}/{}/{}", input.repo, input.collection, rkey), 423 - cid: record_cid.to_string(), 424 - }; 425 - (StatusCode::OK, Json(output)).into_response() 426 - } 427 - 428 - #[derive(Deserialize)] 429 - pub struct GetRecordInput { 430 - pub repo: String, 431 - pub collection: String, 432 - pub rkey: String, 433 - pub cid: Option<String>, 434 - } 435 - 436 - pub async fn get_record( 437 - State(state): State<AppState>, 438 - Query(input): Query<GetRecordInput>, 439 - ) -> Response { 440 - let user_row = if input.repo.starts_with("did:") { 441 - sqlx::query("SELECT id FROM users WHERE did = $1") 442 - .bind(&input.repo) 443 - .fetch_optional(&state.db) 444 - .await 445 - } else { 446 - sqlx::query("SELECT id FROM users WHERE handle = $1") 447 - .bind(&input.repo) 448 - .fetch_optional(&state.db) 449 - .await 450 - }; 451 - 452 - let user_id: uuid::Uuid = match user_row { 453 - Ok(Some(row)) => row.get("id"), 454 - _ => return (StatusCode::NOT_FOUND, Json(json!({"error": "NotFound", "message": "Repo not found"}))).into_response(), 455 - }; 456 - 457 - let record_row = sqlx::query("SELECT record_cid FROM records WHERE repo_id = $1 AND collection = $2 AND rkey = $3") 458 - .bind(user_id) 459 - .bind(&input.collection) 460 - .bind(&input.rkey) 461 - .fetch_optional(&state.db) 462 - .await; 463 - 464 - let record_cid_str: String = match record_row { 465 - Ok(Some(row)) => row.get("record_cid"), 466 - _ => return (StatusCode::NOT_FOUND, Json(json!({"error": "NotFound", "message": "Record not found"}))).into_response(), 467 - }; 468 - 469 - if let Some(expected_cid) = &input.cid { 470 - if &record_cid_str != expected_cid { 471 - return (StatusCode::NOT_FOUND, Json(json!({"error": "NotFound", "message": "Record CID mismatch"}))).into_response(); 472 - } 473 - } 474 - 475 - let cid = match Cid::from_str(&record_cid_str) { 476 - Ok(c) => c, 477 - Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Invalid CID in DB"}))).into_response(), 478 - }; 479 - 480 - let block = match state.block_store.get(&cid).await { 481 - Ok(Some(b)) => b, 482 - _ => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Record block not found"}))).into_response(), 483 - }; 484 - 485 - let value: serde_json::Value = match serde_ipld_dagcbor::from_slice(&block) { 486 - Ok(v) => v, 487 - Err(e) => { 488 - error!("Failed to deserialize record: {:?}", e); 489 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 490 - } 491 - }; 492 - 493 - Json(json!({ 494 - "uri": format!("at://{}/{}/{}", input.repo, input.collection, input.rkey), 495 - "cid": record_cid_str, 496 - "value": value 497 - })).into_response() 498 - } 499 - 500 - #[derive(Deserialize)] 501 - pub struct DeleteRecordInput { 502 - pub repo: String, 503 - pub collection: String, 504 - pub rkey: String, 505 - #[serde(rename = "swapRecord")] 506 - pub swap_record: Option<String>, 507 - #[serde(rename = "swapCommit")] 508 - pub swap_commit: Option<String>, 509 - } 510 - 511 - pub async fn delete_record( 512 - State(state): State<AppState>, 513 - headers: axum::http::HeaderMap, 514 - Json(input): Json<DeleteRecordInput>, 515 - ) -> Response { 516 - let auth_header = headers.get("Authorization"); 517 - if auth_header.is_none() { 518 - return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationRequired"}))).into_response(); 519 - } 520 - let token = auth_header.unwrap().to_str().unwrap_or("").replace("Bearer ", ""); 521 - 522 - let session = sqlx::query( 523 - "SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.access_jwt = $1" 524 - ) 525 - .bind(&token) 526 - .fetch_optional(&state.db) 527 - .await 528 - .unwrap_or(None); 529 - 530 - let (did, key_bytes) = match session { 531 - Some(row) => (row.get::<String, _>("did"), row.get::<Vec<u8>, _>("key_bytes")), 532 - None => return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed"}))).into_response(), 533 - }; 534 - 535 - if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 536 - return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"}))).into_response(); 537 - } 538 - 539 - if input.repo != did { 540 - return (StatusCode::FORBIDDEN, Json(json!({"error": "InvalidRepo", "message": "Repo does not match authenticated user"}))).into_response(); 541 - } 542 - 543 - let user_query = sqlx::query("SELECT id FROM users WHERE did = $1") 544 - .bind(&did) 545 - .fetch_optional(&state.db) 546 - .await; 547 - 548 - let user_id: uuid::Uuid = match user_query { 549 - Ok(Some(row)) => row.get("id"), 550 - _ => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "User not found"}))).into_response(), 551 - }; 552 - 553 - let repo_root_query = sqlx::query("SELECT repo_root_cid FROM repos WHERE user_id = $1") 554 - .bind(user_id) 555 - .fetch_optional(&state.db) 556 - .await; 557 - 558 - let current_root_cid = match repo_root_query { 559 - Ok(Some(row)) => { 560 - let cid_str: String = row.get("repo_root_cid"); 561 - Cid::from_str(&cid_str).ok() 562 - }, 563 - _ => None, 564 - }; 565 - 566 - if current_root_cid.is_none() { 567 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Repo root not found"}))).into_response(); 568 - } 569 - let current_root_cid = current_root_cid.unwrap(); 570 - 571 - let commit_bytes = match state.block_store.get(&current_root_cid).await { 572 - Ok(Some(b)) => b, 573 - Ok(None) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Commit block not found"}))).into_response(), 574 - Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": format!("Failed to load commit block: {:?}", e)}))).into_response(), 575 - }; 576 - 577 - let commit = match Commit::from_cbor(&commit_bytes) { 578 - Ok(c) => c, 579 - Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": format!("Failed to parse commit: {:?}", e)}))).into_response(), 580 - }; 581 - 582 - let mst_root = commit.data; 583 - let store = Arc::new(state.block_store.clone()); 584 - let mst = Mst::load(store.clone(), mst_root, None); 585 - 586 - let collection_nsid = match input.collection.parse::<Nsid>() { 587 - Ok(n) => n, 588 - Err(_) => return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidCollection"}))).into_response(), 589 - }; 590 - 591 - let key = format!("{}/{}", collection_nsid, input.rkey); 592 - 593 - // TODO: Check swapRecord if provided? Skipping for brevity/robustness 594 - 595 - if let Err(e) = mst.delete(&key).await { 596 - error!("Failed to delete from MST: {:?}", e); 597 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": format!("Failed to delete from MST: {:?}", e)}))).into_response(); 598 - } 599 - 600 - let new_mst_root = match mst.root().await { 601 - Ok(c) => c, 602 - Err(_e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to get new MST root"}))).into_response(), 603 - }; 604 - 605 - let did_obj = match Did::new(&did) { 606 - Ok(d) => d, 607 - Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Invalid DID"}))).into_response(), 608 - }; 609 - 610 - let rev = Tid::now(LimitedU32::MIN); 611 - 612 - let new_commit = Commit::new_unsigned( 613 - did_obj, 614 - new_mst_root, 615 - rev, 616 - Some(current_root_cid) 617 - ); 618 - 619 - let new_commit_bytes = match new_commit.to_cbor() { 620 - Ok(b) => b, 621 - Err(_e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to serialize new commit"}))).into_response(), 622 - }; 623 - 624 - let new_root_cid = match state.block_store.put(&new_commit_bytes).await { 625 - Ok(c) => c, 626 - Err(_e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to save new commit"}))).into_response(), 627 - }; 628 - 629 - let update_repo = sqlx::query("UPDATE repos SET repo_root_cid = $1 WHERE user_id = $2") 630 - .bind(new_root_cid.to_string()) 631 - .bind(user_id) 632 - .execute(&state.db) 633 - .await; 634 - 635 - if let Err(e) = update_repo { 636 - error!("Failed to update repo root in DB: {:?}", e); 637 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to update repo root in DB"}))).into_response(); 638 - } 639 - 640 - let record_delete = sqlx::query("DELETE FROM records WHERE repo_id = $1 AND collection = $2 AND rkey = $3") 641 - .bind(user_id) 642 - .bind(&input.collection) 643 - .bind(&input.rkey) 644 - .execute(&state.db) 645 - .await; 646 - 647 - if let Err(e) = record_delete { 648 - error!("Error deleting record index: {:?}", e); 649 - } 650 - 651 - (StatusCode::OK, Json(json!({}))).into_response() 652 - } 653 - 654 - #[derive(Deserialize)] 655 - pub struct ListRecordsInput { 656 - pub repo: String, 657 - pub collection: String, 658 - pub limit: Option<i32>, 659 - pub cursor: Option<String>, 660 - #[serde(rename = "rkeyStart")] 661 - pub rkey_start: Option<String>, 662 - #[serde(rename = "rkeyEnd")] 663 - pub rkey_end: Option<String>, 664 - pub reverse: Option<bool>, 665 - } 666 - 667 - #[derive(Serialize)] 668 - pub struct ListRecordsOutput { 669 - pub cursor: Option<String>, 670 - pub records: Vec<serde_json::Value>, 671 - } 672 - 673 - pub async fn list_records( 674 - State(state): State<AppState>, 675 - Query(input): Query<ListRecordsInput>, 676 - ) -> Response { 677 - let user_row = if input.repo.starts_with("did:") { 678 - sqlx::query("SELECT id FROM users WHERE did = $1") 679 - .bind(&input.repo) 680 - .fetch_optional(&state.db) 681 - .await 682 - } else { 683 - sqlx::query("SELECT id FROM users WHERE handle = $1") 684 - .bind(&input.repo) 685 - .fetch_optional(&state.db) 686 - .await 687 - }; 688 - 689 - let user_id: uuid::Uuid = match user_row { 690 - Ok(Some(row)) => row.get("id"), 691 - _ => return (StatusCode::NOT_FOUND, Json(json!({"error": "NotFound", "message": "Repo not found"}))).into_response(), 692 - }; 693 - 694 - let limit = input.limit.unwrap_or(50).clamp(1, 100); 695 - let reverse = input.reverse.unwrap_or(false); 696 - 697 - // Simplistic query construction - no sophisticated cursor handling or rkey ranges for now, just basic pagination 698 - // TODO: Implement rkeyStart/End and correct cursor logic 699 - 700 - let query_str = format!( 701 - "SELECT rkey, record_cid FROM records WHERE repo_id = $1 AND collection = $2 {} ORDER BY rkey {} LIMIT {}", 702 - if let Some(_c) = &input.cursor { 703 - if reverse { "AND rkey < $3" } else { "AND rkey > $3" } 704 - } else { 705 - "" 706 - }, 707 - if reverse { "DESC" } else { "ASC" }, 708 - limit 709 - ); 710 - 711 - let mut query = sqlx::query(&query_str) 712 - .bind(user_id) 713 - .bind(&input.collection); 714 - 715 - if let Some(c) = &input.cursor { 716 - query = query.bind(c); 717 - } 718 - 719 - let rows = match query.fetch_all(&state.db).await { 720 - Ok(r) => r, 721 - Err(e) => { 722 - error!("Error listing records: {:?}", e); 723 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 724 - } 725 - }; 726 - 727 - let mut records = Vec::new(); 728 - let mut last_rkey = None; 729 - 730 - for row in rows { 731 - let rkey: String = row.get("rkey"); 732 - let cid_str: String = row.get("record_cid"); 733 - last_rkey = Some(rkey.clone()); 734 - 735 - if let Ok(cid) = Cid::from_str(&cid_str) { 736 - if let Ok(Some(block)) = state.block_store.get(&cid).await { 737 - if let Ok(value) = serde_ipld_dagcbor::from_slice::<serde_json::Value>(&block) { 738 - records.push(json!({ 739 - "uri": format!("at://{}/{}/{}", input.repo, input.collection, rkey), 740 - "cid": cid_str, 741 - "value": value 742 - })); 743 - } 744 - } 745 - } 746 - } 747 - 748 - Json(ListRecordsOutput { 749 - cursor: last_rkey, 750 - records, 751 - }).into_response() 752 - } 753 - 754 - #[derive(Deserialize)] 755 - pub struct DescribeRepoInput { 756 - pub repo: String, 757 - } 758 - 759 - pub async fn describe_repo( 760 - State(state): State<AppState>, 761 - Query(input): Query<DescribeRepoInput>, 762 - ) -> Response { 763 - let user_row = if input.repo.starts_with("did:") { 764 - sqlx::query("SELECT id, handle, did FROM users WHERE did = $1") 765 - .bind(&input.repo) 766 - .fetch_optional(&state.db) 767 - .await 768 - } else { 769 - sqlx::query("SELECT id, handle, did FROM users WHERE handle = $1") 770 - .bind(&input.repo) 771 - .fetch_optional(&state.db) 772 - .await 773 - }; 774 - 775 - let (user_id, handle, did) = match user_row { 776 - Ok(Some(row)) => (row.get::<uuid::Uuid, _>("id"), row.get::<String, _>("handle"), row.get::<String, _>("did")), 777 - _ => return (StatusCode::NOT_FOUND, Json(json!({"error": "NotFound", "message": "Repo not found"}))).into_response(), 778 - }; 779 - 780 - let collections_query = sqlx::query("SELECT DISTINCT collection FROM records WHERE repo_id = $1") 781 - .bind(user_id) 782 - .fetch_all(&state.db) 783 - .await; 784 - 785 - let collections: Vec<String> = match collections_query { 786 - Ok(rows) => rows.iter().map(|r| r.get("collection")).collect(), 787 - Err(_) => Vec::new(), 788 - }; 789 - 790 - let did_doc = json!({ 791 - "id": did, 792 - "alsoKnownAs": [format!("at://{}", handle)] 793 - }); 794 - 795 - Json(json!({ 796 - "handle": handle, 797 - "did": did, 798 - "didDoc": did_doc, 799 - "collections": collections, 800 - "handleIsCorrect": true 801 - })).into_response() 802 - } 803 - 804 - pub async fn upload_blob( 805 - State(state): State<AppState>, 806 - headers: axum::http::HeaderMap, 807 - body: Bytes, 808 - ) -> Response { 809 - let auth_header = headers.get("Authorization"); 810 - if auth_header.is_none() { 811 - return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationRequired"}))).into_response(); 812 - } 813 - let token = auth_header.unwrap().to_str().unwrap_or("").replace("Bearer ", ""); 814 - 815 - let session = sqlx::query( 816 - "SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.access_jwt = $1" 817 - ) 818 - .bind(&token) 819 - .fetch_optional(&state.db) 820 - .await 821 - .unwrap_or(None); 822 - 823 - let (did, key_bytes) = match session { 824 - Some(row) => (row.get::<String, _>("did"), row.get::<Vec<u8>, _>("key_bytes")), 825 - None => return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed"}))).into_response(), 826 - }; 827 - 828 - if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 829 - return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"}))).into_response(); 830 - } 831 - 832 - let mime_type = headers.get("content-type") 833 - .and_then(|h| h.to_str().ok()) 834 - .unwrap_or("application/octet-stream") 835 - .to_string(); 836 - 837 - let size = body.len() as i64; 838 - let data = body.to_vec(); 839 - 840 - let mut hasher = Sha256::new(); 841 - hasher.update(&data); 842 - let hash = hasher.finalize(); 843 - let multihash = Multihash::wrap(0x12, &hash).unwrap(); 844 - let cid = Cid::new_v1(0x55, multihash); 845 - let cid_str = cid.to_string(); 846 - 847 - let storage_key = format!("blobs/{}", cid_str); 848 - 849 - if let Err(e) = state.blob_store.put(&storage_key, &data).await { 850 - error!("Failed to upload blob to storage: {:?}", e); 851 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to store blob"}))).into_response(); 852 - } 853 - 854 - let user_query = sqlx::query("SELECT id FROM users WHERE did = $1") 855 - .bind(&did) 856 - .fetch_optional(&state.db) 857 - .await; 858 - 859 - let user_id: uuid::Uuid = match user_query { 860 - Ok(Some(row)) => row.get("id"), 861 - _ => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(), 862 - }; 863 - 864 - let insert = sqlx::query( 865 - "INSERT INTO blobs (cid, mime_type, size_bytes, created_by_user, storage_key) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (cid) DO NOTHING" 866 - ) 867 - .bind(&cid_str) 868 - .bind(&mime_type) 869 - .bind(size) 870 - .bind(user_id) 871 - .bind(&storage_key) 872 - .execute(&state.db) 873 - .await; 874 - 875 - if let Err(e) = insert { 876 - error!("Failed to insert blob record: {:?}", e); 877 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 878 - } 879 - 880 - Json(json!({ 881 - "blob": { 882 - "ref": { 883 - "$link": cid_str 884 - }, 885 - "mimeType": mime_type, 886 - "size": size 887 - } 888 - })).into_response() 889 - }
···
+138
src/api/repo/blob.rs
···
··· 1 + use crate::state::AppState; 2 + use axum::body::Bytes; 3 + use axum::{ 4 + Json, 5 + extract::State, 6 + http::StatusCode, 7 + response::{IntoResponse, Response}, 8 + }; 9 + use cid::Cid; 10 + use multihash::Multihash; 11 + use serde_json::json; 12 + use sha2::{Digest, Sha256}; 13 + use sqlx::Row; 14 + use tracing::error; 15 + 16 + pub async fn upload_blob( 17 + State(state): State<AppState>, 18 + headers: axum::http::HeaderMap, 19 + body: Bytes, 20 + ) -> Response { 21 + let auth_header = headers.get("Authorization"); 22 + if auth_header.is_none() { 23 + return ( 24 + StatusCode::UNAUTHORIZED, 25 + Json(json!({"error": "AuthenticationRequired"})), 26 + ) 27 + .into_response(); 28 + } 29 + let token = auth_header 30 + .unwrap() 31 + .to_str() 32 + .unwrap_or("") 33 + .replace("Bearer ", ""); 34 + 35 + let session = sqlx::query( 36 + "SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.access_jwt = $1" 37 + ) 38 + .bind(&token) 39 + .fetch_optional(&state.db) 40 + .await 41 + .unwrap_or(None); 42 + 43 + let (did, key_bytes) = match session { 44 + Some(row) => ( 45 + row.get::<String, _>("did"), 46 + row.get::<Vec<u8>, _>("key_bytes"), 47 + ), 48 + None => { 49 + return ( 50 + StatusCode::UNAUTHORIZED, 51 + Json(json!({"error": "AuthenticationFailed"})), 52 + ) 53 + .into_response(); 54 + } 55 + }; 56 + 57 + if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 58 + return ( 59 + StatusCode::UNAUTHORIZED, 60 + Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})), 61 + ) 62 + .into_response(); 63 + } 64 + 65 + let mime_type = headers 66 + .get("content-type") 67 + .and_then(|h| h.to_str().ok()) 68 + .unwrap_or("application/octet-stream") 69 + .to_string(); 70 + 71 + let size = body.len() as i64; 72 + let data = body.to_vec(); 73 + 74 + let mut hasher = Sha256::new(); 75 + hasher.update(&data); 76 + let hash = hasher.finalize(); 77 + let multihash = Multihash::wrap(0x12, &hash).unwrap(); 78 + let cid = Cid::new_v1(0x55, multihash); 79 + let cid_str = cid.to_string(); 80 + 81 + let storage_key = format!("blobs/{}", cid_str); 82 + 83 + if let Err(e) = state.blob_store.put(&storage_key, &data).await { 84 + error!("Failed to upload blob to storage: {:?}", e); 85 + return ( 86 + StatusCode::INTERNAL_SERVER_ERROR, 87 + Json(json!({"error": "InternalError", "message": "Failed to store blob"})), 88 + ) 89 + .into_response(); 90 + } 91 + 92 + let user_query = sqlx::query("SELECT id FROM users WHERE did = $1") 93 + .bind(&did) 94 + .fetch_optional(&state.db) 95 + .await; 96 + 97 + let user_id: uuid::Uuid = match user_query { 98 + Ok(Some(row)) => row.get("id"), 99 + _ => { 100 + return ( 101 + StatusCode::INTERNAL_SERVER_ERROR, 102 + Json(json!({"error": "InternalError"})), 103 + ) 104 + .into_response(); 105 + } 106 + }; 107 + 108 + let insert = sqlx::query( 109 + "INSERT INTO blobs (cid, mime_type, size_bytes, created_by_user, storage_key) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (cid) DO NOTHING" 110 + ) 111 + .bind(&cid_str) 112 + .bind(&mime_type) 113 + .bind(size) 114 + .bind(user_id) 115 + .bind(&storage_key) 116 + .execute(&state.db) 117 + .await; 118 + 119 + if let Err(e) = insert { 120 + error!("Failed to insert blob record: {:?}", e); 121 + return ( 122 + StatusCode::INTERNAL_SERVER_ERROR, 123 + Json(json!({"error": "InternalError"})), 124 + ) 125 + .into_response(); 126 + } 127 + 128 + Json(json!({ 129 + "blob": { 130 + "ref": { 131 + "$link": cid_str 132 + }, 133 + "mimeType": mime_type, 134 + "size": size 135 + } 136 + })) 137 + .into_response() 138 + }
+72
src/api/repo/meta.rs
···
··· 1 + use crate::state::AppState; 2 + use axum::{ 3 + Json, 4 + extract::{Query, State}, 5 + http::StatusCode, 6 + response::{IntoResponse, Response}, 7 + }; 8 + use serde::Deserialize; 9 + use serde_json::json; 10 + use sqlx::Row; 11 + 12 + #[derive(Deserialize)] 13 + pub struct DescribeRepoInput { 14 + pub repo: String, 15 + } 16 + 17 + pub async fn describe_repo( 18 + State(state): State<AppState>, 19 + Query(input): Query<DescribeRepoInput>, 20 + ) -> Response { 21 + let user_row = if input.repo.starts_with("did:") { 22 + sqlx::query("SELECT id, handle, did FROM users WHERE did = $1") 23 + .bind(&input.repo) 24 + .fetch_optional(&state.db) 25 + .await 26 + } else { 27 + sqlx::query("SELECT id, handle, did FROM users WHERE handle = $1") 28 + .bind(&input.repo) 29 + .fetch_optional(&state.db) 30 + .await 31 + }; 32 + 33 + let (user_id, handle, did) = match user_row { 34 + Ok(Some(row)) => ( 35 + row.get::<uuid::Uuid, _>("id"), 36 + row.get::<String, _>("handle"), 37 + row.get::<String, _>("did"), 38 + ), 39 + _ => { 40 + return ( 41 + StatusCode::NOT_FOUND, 42 + Json(json!({"error": "NotFound", "message": "Repo not found"})), 43 + ) 44 + .into_response(); 45 + } 46 + }; 47 + 48 + let collections_query = 49 + sqlx::query("SELECT DISTINCT collection FROM records WHERE repo_id = $1") 50 + .bind(user_id) 51 + .fetch_all(&state.db) 52 + .await; 53 + 54 + let collections: Vec<String> = match collections_query { 55 + Ok(rows) => rows.iter().map(|r| r.get("collection")).collect(), 56 + Err(_) => Vec::new(), 57 + }; 58 + 59 + let did_doc = json!({ 60 + "id": did, 61 + "alsoKnownAs": [format!("at://{}", handle)] 62 + }); 63 + 64 + Json(json!({ 65 + "handle": handle, 66 + "did": did, 67 + "didDoc": did_doc, 68 + "collections": collections, 69 + "handleIsCorrect": true 70 + })) 71 + .into_response() 72 + }
+7
src/api/repo/mod.rs
···
··· 1 + pub mod blob; 2 + pub mod meta; 3 + pub mod record; 4 + 5 + pub use blob::upload_blob; 6 + pub use meta::describe_repo; 7 + pub use record::{create_record, delete_record, get_record, list_records, put_record};
+236
src/api/repo/record/delete.rs
···
··· 1 + use crate::state::AppState; 2 + use axum::{ 3 + Json, 4 + extract::State, 5 + http::StatusCode, 6 + response::{IntoResponse, Response}, 7 + }; 8 + use cid::Cid; 9 + use jacquard::types::{ 10 + did::Did, 11 + integer::LimitedU32, 12 + string::{Nsid, Tid}, 13 + }; 14 + use jacquard_repo::{commit::Commit, mst::Mst, storage::BlockStore}; 15 + use serde::Deserialize; 16 + use serde_json::json; 17 + use sqlx::Row; 18 + use std::str::FromStr; 19 + use std::sync::Arc; 20 + use tracing::error; 21 + 22 + #[derive(Deserialize)] 23 + pub struct DeleteRecordInput { 24 + pub repo: String, 25 + pub collection: String, 26 + pub rkey: String, 27 + #[serde(rename = "swapRecord")] 28 + pub swap_record: Option<String>, 29 + #[serde(rename = "swapCommit")] 30 + pub swap_commit: Option<String>, 31 + } 32 + 33 + pub async fn delete_record( 34 + State(state): State<AppState>, 35 + headers: axum::http::HeaderMap, 36 + Json(input): Json<DeleteRecordInput>, 37 + ) -> Response { 38 + let auth_header = headers.get("Authorization"); 39 + if auth_header.is_none() { 40 + return ( 41 + StatusCode::UNAUTHORIZED, 42 + Json(json!({"error": "AuthenticationRequired"})), 43 + ) 44 + .into_response(); 45 + } 46 + let token = auth_header 47 + .unwrap() 48 + .to_str() 49 + .unwrap_or("") 50 + .replace("Bearer ", ""); 51 + 52 + let session = sqlx::query( 53 + "SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.access_jwt = $1" 54 + ) 55 + .bind(&token) 56 + .fetch_optional(&state.db) 57 + .await 58 + .unwrap_or(None); 59 + 60 + let (did, key_bytes) = match session { 61 + Some(row) => ( 62 + row.get::<String, _>("did"), 63 + row.get::<Vec<u8>, _>("key_bytes"), 64 + ), 65 + None => { 66 + return ( 67 + StatusCode::UNAUTHORIZED, 68 + Json(json!({"error": "AuthenticationFailed"})), 69 + ) 70 + .into_response(); 71 + } 72 + }; 73 + 74 + if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 75 + return ( 76 + StatusCode::UNAUTHORIZED, 77 + Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})), 78 + ) 79 + .into_response(); 80 + } 81 + 82 + if input.repo != did { 83 + return (StatusCode::FORBIDDEN, Json(json!({"error": "InvalidRepo", "message": "Repo does not match authenticated user"}))).into_response(); 84 + } 85 + 86 + let user_query = sqlx::query("SELECT id FROM users WHERE did = $1") 87 + .bind(&did) 88 + .fetch_optional(&state.db) 89 + .await; 90 + 91 + let user_id: uuid::Uuid = match user_query { 92 + Ok(Some(row)) => row.get("id"), 93 + _ => { 94 + return ( 95 + StatusCode::INTERNAL_SERVER_ERROR, 96 + Json(json!({"error": "InternalError", "message": "User not found"})), 97 + ) 98 + .into_response(); 99 + } 100 + }; 101 + 102 + let repo_root_query = sqlx::query("SELECT repo_root_cid FROM repos WHERE user_id = $1") 103 + .bind(user_id) 104 + .fetch_optional(&state.db) 105 + .await; 106 + 107 + let current_root_cid = match repo_root_query { 108 + Ok(Some(row)) => { 109 + let cid_str: String = row.get("repo_root_cid"); 110 + Cid::from_str(&cid_str).ok() 111 + } 112 + _ => None, 113 + }; 114 + 115 + if current_root_cid.is_none() { 116 + return ( 117 + StatusCode::INTERNAL_SERVER_ERROR, 118 + Json(json!({"error": "InternalError", "message": "Repo root not found"})), 119 + ) 120 + .into_response(); 121 + } 122 + let current_root_cid = current_root_cid.unwrap(); 123 + 124 + let commit_bytes = match state.block_store.get(&current_root_cid).await { 125 + Ok(Some(b)) => b, 126 + Ok(None) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Commit block not found"}))).into_response(), 127 + Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": format!("Failed to load commit block: {:?}", e)}))).into_response(), 128 + }; 129 + 130 + let commit = match Commit::from_cbor(&commit_bytes) { 131 + Ok(c) => c, 132 + Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": format!("Failed to parse commit: {:?}", e)}))).into_response(), 133 + }; 134 + 135 + let mst_root = commit.data; 136 + let store = Arc::new(state.block_store.clone()); 137 + let mst = Mst::load(store.clone(), mst_root, None); 138 + 139 + let collection_nsid = match input.collection.parse::<Nsid>() { 140 + Ok(n) => n, 141 + Err(_) => { 142 + return ( 143 + StatusCode::BAD_REQUEST, 144 + Json(json!({"error": "InvalidCollection"})), 145 + ) 146 + .into_response(); 147 + } 148 + }; 149 + 150 + let key = format!("{}/{}", collection_nsid, input.rkey); 151 + 152 + // TODO: Check swapRecord if provided? Skipping for brevity/robustness 153 + 154 + if let Err(e) = mst.delete(&key).await { 155 + error!("Failed to delete from MST: {:?}", e); 156 + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": format!("Failed to delete from MST: {:?}", e)}))).into_response(); 157 + } 158 + 159 + let new_mst_root = match mst.root().await { 160 + Ok(c) => c, 161 + Err(_e) => { 162 + return ( 163 + StatusCode::INTERNAL_SERVER_ERROR, 164 + Json(json!({"error": "InternalError", "message": "Failed to get new MST root"})), 165 + ) 166 + .into_response(); 167 + } 168 + }; 169 + 170 + let did_obj = match Did::new(&did) { 171 + Ok(d) => d, 172 + Err(_) => { 173 + return ( 174 + StatusCode::INTERNAL_SERVER_ERROR, 175 + Json(json!({"error": "InternalError", "message": "Invalid DID"})), 176 + ) 177 + .into_response(); 178 + } 179 + }; 180 + 181 + let rev = Tid::now(LimitedU32::MIN); 182 + 183 + let new_commit = Commit::new_unsigned(did_obj, new_mst_root, rev, Some(current_root_cid)); 184 + 185 + let new_commit_bytes = 186 + match new_commit.to_cbor() { 187 + Ok(b) => b, 188 + Err(_e) => return ( 189 + StatusCode::INTERNAL_SERVER_ERROR, 190 + Json( 191 + json!({"error": "InternalError", "message": "Failed to serialize new commit"}), 192 + ), 193 + ) 194 + .into_response(), 195 + }; 196 + 197 + let new_root_cid = match state.block_store.put(&new_commit_bytes).await { 198 + Ok(c) => c, 199 + Err(_e) => { 200 + return ( 201 + StatusCode::INTERNAL_SERVER_ERROR, 202 + Json(json!({"error": "InternalError", "message": "Failed to save new commit"})), 203 + ) 204 + .into_response(); 205 + } 206 + }; 207 + 208 + let update_repo = sqlx::query("UPDATE repos SET repo_root_cid = $1 WHERE user_id = $2") 209 + .bind(new_root_cid.to_string()) 210 + .bind(user_id) 211 + .execute(&state.db) 212 + .await; 213 + 214 + if let Err(e) = update_repo { 215 + error!("Failed to update repo root in DB: {:?}", e); 216 + return ( 217 + StatusCode::INTERNAL_SERVER_ERROR, 218 + Json(json!({"error": "InternalError", "message": "Failed to update repo root in DB"})), 219 + ) 220 + .into_response(); 221 + } 222 + 223 + let record_delete = 224 + sqlx::query("DELETE FROM records WHERE repo_id = $1 AND collection = $2 AND rkey = $3") 225 + .bind(user_id) 226 + .bind(&input.collection) 227 + .bind(&input.rkey) 228 + .execute(&state.db) 229 + .await; 230 + 231 + if let Err(e) = record_delete { 232 + error!("Error deleting record index: {:?}", e); 233 + } 234 + 235 + (StatusCode::OK, Json(json!({}))).into_response() 236 + }
+10
src/api/repo/record/mod.rs
···
··· 1 + pub mod delete; 2 + pub mod read; 3 + pub mod write; 4 + 5 + pub use delete::{DeleteRecordInput, delete_record}; 6 + pub use read::{GetRecordInput, ListRecordsInput, ListRecordsOutput, get_record, list_records}; 7 + pub use write::{ 8 + CreateRecordInput, CreateRecordOutput, PutRecordInput, PutRecordOutput, create_record, 9 + put_record, 10 + };
+236
src/api/repo/record/read.rs
···
··· 1 + use crate::state::AppState; 2 + use axum::{ 3 + Json, 4 + extract::{Query, State}, 5 + http::StatusCode, 6 + response::{IntoResponse, Response}, 7 + }; 8 + use cid::Cid; 9 + use jacquard_repo::storage::BlockStore; 10 + use serde::{Deserialize, Serialize}; 11 + use serde_json::json; 12 + use sqlx::Row; 13 + use std::str::FromStr; 14 + use tracing::error; 15 + 16 + #[derive(Deserialize)] 17 + pub struct GetRecordInput { 18 + pub repo: String, 19 + pub collection: String, 20 + pub rkey: String, 21 + pub cid: Option<String>, 22 + } 23 + 24 + pub async fn get_record( 25 + State(state): State<AppState>, 26 + Query(input): Query<GetRecordInput>, 27 + ) -> Response { 28 + let user_row = if input.repo.starts_with("did:") { 29 + sqlx::query("SELECT id FROM users WHERE did = $1") 30 + .bind(&input.repo) 31 + .fetch_optional(&state.db) 32 + .await 33 + } else { 34 + sqlx::query("SELECT id FROM users WHERE handle = $1") 35 + .bind(&input.repo) 36 + .fetch_optional(&state.db) 37 + .await 38 + }; 39 + 40 + let user_id: uuid::Uuid = match user_row { 41 + Ok(Some(row)) => row.get("id"), 42 + _ => { 43 + return ( 44 + StatusCode::NOT_FOUND, 45 + Json(json!({"error": "NotFound", "message": "Repo not found"})), 46 + ) 47 + .into_response(); 48 + } 49 + }; 50 + 51 + let record_row = sqlx::query( 52 + "SELECT record_cid FROM records WHERE repo_id = $1 AND collection = $2 AND rkey = $3", 53 + ) 54 + .bind(user_id) 55 + .bind(&input.collection) 56 + .bind(&input.rkey) 57 + .fetch_optional(&state.db) 58 + .await; 59 + 60 + let record_cid_str: String = match record_row { 61 + Ok(Some(row)) => row.get("record_cid"), 62 + _ => { 63 + return ( 64 + StatusCode::NOT_FOUND, 65 + Json(json!({"error": "NotFound", "message": "Record not found"})), 66 + ) 67 + .into_response(); 68 + } 69 + }; 70 + 71 + if let Some(expected_cid) = &input.cid { 72 + if &record_cid_str != expected_cid { 73 + return ( 74 + StatusCode::NOT_FOUND, 75 + Json(json!({"error": "NotFound", "message": "Record CID mismatch"})), 76 + ) 77 + .into_response(); 78 + } 79 + } 80 + 81 + let cid = match Cid::from_str(&record_cid_str) { 82 + Ok(c) => c, 83 + Err(_) => { 84 + return ( 85 + StatusCode::INTERNAL_SERVER_ERROR, 86 + Json(json!({"error": "InternalError", "message": "Invalid CID in DB"})), 87 + ) 88 + .into_response(); 89 + } 90 + }; 91 + 92 + let block = match state.block_store.get(&cid).await { 93 + Ok(Some(b)) => b, 94 + _ => { 95 + return ( 96 + StatusCode::INTERNAL_SERVER_ERROR, 97 + Json(json!({"error": "InternalError", "message": "Record block not found"})), 98 + ) 99 + .into_response(); 100 + } 101 + }; 102 + 103 + let value: serde_json::Value = match serde_ipld_dagcbor::from_slice(&block) { 104 + Ok(v) => v, 105 + Err(e) => { 106 + error!("Failed to deserialize record: {:?}", e); 107 + return ( 108 + StatusCode::INTERNAL_SERVER_ERROR, 109 + Json(json!({"error": "InternalError"})), 110 + ) 111 + .into_response(); 112 + } 113 + }; 114 + 115 + Json(json!({ 116 + "uri": format!("at://{}/{}/{}", input.repo, input.collection, input.rkey), 117 + "cid": record_cid_str, 118 + "value": value 119 + })) 120 + .into_response() 121 + } 122 + 123 + #[derive(Deserialize)] 124 + pub struct ListRecordsInput { 125 + pub repo: String, 126 + pub collection: String, 127 + pub limit: Option<i32>, 128 + pub cursor: Option<String>, 129 + #[serde(rename = "rkeyStart")] 130 + pub rkey_start: Option<String>, 131 + #[serde(rename = "rkeyEnd")] 132 + pub rkey_end: Option<String>, 133 + pub reverse: Option<bool>, 134 + } 135 + 136 + #[derive(Serialize)] 137 + pub struct ListRecordsOutput { 138 + pub cursor: Option<String>, 139 + pub records: Vec<serde_json::Value>, 140 + } 141 + 142 + pub async fn list_records( 143 + State(state): State<AppState>, 144 + Query(input): Query<ListRecordsInput>, 145 + ) -> Response { 146 + let user_row = if input.repo.starts_with("did:") { 147 + sqlx::query("SELECT id FROM users WHERE did = $1") 148 + .bind(&input.repo) 149 + .fetch_optional(&state.db) 150 + .await 151 + } else { 152 + sqlx::query("SELECT id FROM users WHERE handle = $1") 153 + .bind(&input.repo) 154 + .fetch_optional(&state.db) 155 + .await 156 + }; 157 + 158 + let user_id: uuid::Uuid = match user_row { 159 + Ok(Some(row)) => row.get("id"), 160 + _ => { 161 + return ( 162 + StatusCode::NOT_FOUND, 163 + Json(json!({"error": "NotFound", "message": "Repo not found"})), 164 + ) 165 + .into_response(); 166 + } 167 + }; 168 + 169 + let limit = input.limit.unwrap_or(50).clamp(1, 100); 170 + let reverse = input.reverse.unwrap_or(false); 171 + 172 + // Simplistic query construction - no sophisticated cursor handling or rkey ranges for now, just basic pagination 173 + // TODO: Implement rkeyStart/End and correct cursor logic 174 + 175 + let query_str = format!( 176 + "SELECT rkey, record_cid FROM records WHERE repo_id = $1 AND collection = $2 {} ORDER BY rkey {} LIMIT {}", 177 + if let Some(_c) = &input.cursor { 178 + if reverse { 179 + "AND rkey < $3" 180 + } else { 181 + "AND rkey > $3" 182 + } 183 + } else { 184 + "" 185 + }, 186 + if reverse { "DESC" } else { "ASC" }, 187 + limit 188 + ); 189 + 190 + let mut query = sqlx::query(&query_str) 191 + .bind(user_id) 192 + .bind(&input.collection); 193 + 194 + if let Some(c) = &input.cursor { 195 + query = query.bind(c); 196 + } 197 + 198 + let rows = match query.fetch_all(&state.db).await { 199 + Ok(r) => r, 200 + Err(e) => { 201 + error!("Error listing records: {:?}", e); 202 + return ( 203 + StatusCode::INTERNAL_SERVER_ERROR, 204 + Json(json!({"error": "InternalError"})), 205 + ) 206 + .into_response(); 207 + } 208 + }; 209 + 210 + let mut records = Vec::new(); 211 + let mut last_rkey = None; 212 + 213 + for row in rows { 214 + let rkey: String = row.get("rkey"); 215 + let cid_str: String = row.get("record_cid"); 216 + last_rkey = Some(rkey.clone()); 217 + 218 + if let Ok(cid) = Cid::from_str(&cid_str) { 219 + if let Ok(Some(block)) = state.block_store.get(&cid).await { 220 + if let Ok(value) = serde_ipld_dagcbor::from_slice::<serde_json::Value>(&block) { 221 + records.push(json!({ 222 + "uri": format!("at://{}/{}/{}", input.repo, input.collection, rkey), 223 + "cid": cid_str, 224 + "value": value 225 + })); 226 + } 227 + } 228 + } 229 + } 230 + 231 + Json(ListRecordsOutput { 232 + cursor: last_rkey, 233 + records, 234 + }) 235 + .into_response() 236 + }
+591
src/api/repo/record/write.rs
···
··· 1 + use crate::state::AppState; 2 + use axum::{ 3 + Json, 4 + extract::State, 5 + http::StatusCode, 6 + response::{IntoResponse, Response}, 7 + }; 8 + use chrono::Utc; 9 + use cid::Cid; 10 + use jacquard::types::{ 11 + did::Did, 12 + integer::LimitedU32, 13 + string::{Nsid, Tid}, 14 + }; 15 + use jacquard_repo::{commit::Commit, mst::Mst, storage::BlockStore}; 16 + use serde::{Deserialize, Serialize}; 17 + use serde_json::json; 18 + use sqlx::Row; 19 + use std::str::FromStr; 20 + use std::sync::Arc; 21 + use tracing::error; 22 + 23 + #[derive(Deserialize)] 24 + #[allow(dead_code)] 25 + pub struct CreateRecordInput { 26 + pub repo: String, 27 + pub collection: String, 28 + pub rkey: Option<String>, 29 + pub validate: Option<bool>, 30 + pub record: serde_json::Value, 31 + #[serde(rename = "swapCommit")] 32 + pub swap_commit: Option<String>, 33 + } 34 + 35 + #[derive(Serialize)] 36 + #[serde(rename_all = "camelCase")] 37 + pub struct CreateRecordOutput { 38 + pub uri: String, 39 + pub cid: String, 40 + } 41 + 42 + pub async fn create_record( 43 + State(state): State<AppState>, 44 + headers: axum::http::HeaderMap, 45 + Json(input): Json<CreateRecordInput>, 46 + ) -> Response { 47 + let auth_header = headers.get("Authorization"); 48 + if auth_header.is_none() { 49 + return ( 50 + StatusCode::UNAUTHORIZED, 51 + Json(json!({"error": "AuthenticationRequired"})), 52 + ) 53 + .into_response(); 54 + } 55 + let token = auth_header 56 + .unwrap() 57 + .to_str() 58 + .unwrap_or("") 59 + .replace("Bearer ", ""); 60 + 61 + let session = sqlx::query( 62 + "SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.access_jwt = $1" 63 + ) 64 + .bind(&token) 65 + .fetch_optional(&state.db) 66 + .await 67 + .unwrap_or(None); 68 + 69 + let (did, key_bytes) = match session { 70 + Some(row) => ( 71 + row.get::<String, _>("did"), 72 + row.get::<Vec<u8>, _>("key_bytes"), 73 + ), 74 + None => { 75 + return ( 76 + StatusCode::UNAUTHORIZED, 77 + Json(json!({"error": "AuthenticationFailed"})), 78 + ) 79 + .into_response(); 80 + } 81 + }; 82 + 83 + if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 84 + return ( 85 + StatusCode::UNAUTHORIZED, 86 + Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})), 87 + ) 88 + .into_response(); 89 + } 90 + 91 + if input.repo != did { 92 + return (StatusCode::FORBIDDEN, Json(json!({"error": "InvalidRepo", "message": "Repo does not match authenticated user"}))).into_response(); 93 + } 94 + 95 + let user_query = sqlx::query("SELECT id FROM users WHERE did = $1") 96 + .bind(&did) 97 + .fetch_optional(&state.db) 98 + .await; 99 + 100 + let user_id: uuid::Uuid = match user_query { 101 + Ok(Some(row)) => row.get("id"), 102 + _ => { 103 + return ( 104 + StatusCode::INTERNAL_SERVER_ERROR, 105 + Json(json!({"error": "InternalError", "message": "User not found"})), 106 + ) 107 + .into_response(); 108 + } 109 + }; 110 + 111 + let repo_root_query = sqlx::query("SELECT repo_root_cid FROM repos WHERE user_id = $1") 112 + .bind(user_id) 113 + .fetch_optional(&state.db) 114 + .await; 115 + 116 + let current_root_cid = match repo_root_query { 117 + Ok(Some(row)) => { 118 + let cid_str: String = row.get("repo_root_cid"); 119 + Cid::from_str(&cid_str).ok() 120 + } 121 + _ => None, 122 + }; 123 + 124 + if current_root_cid.is_none() { 125 + error!("Repo root not found for user {}", did); 126 + return ( 127 + StatusCode::INTERNAL_SERVER_ERROR, 128 + Json(json!({"error": "InternalError", "message": "Repo root not found"})), 129 + ) 130 + .into_response(); 131 + } 132 + let current_root_cid = current_root_cid.unwrap(); 133 + 134 + let commit_bytes = match state.block_store.get(&current_root_cid).await { 135 + Ok(Some(b)) => b, 136 + Ok(None) => { 137 + error!("Commit block not found: {}", current_root_cid); 138 + return ( 139 + StatusCode::INTERNAL_SERVER_ERROR, 140 + Json(json!({"error": "InternalError"})), 141 + ) 142 + .into_response(); 143 + } 144 + Err(e) => { 145 + error!("Failed to load commit block: {:?}", e); 146 + return ( 147 + StatusCode::INTERNAL_SERVER_ERROR, 148 + Json(json!({"error": "InternalError"})), 149 + ) 150 + .into_response(); 151 + } 152 + }; 153 + 154 + let commit = match Commit::from_cbor(&commit_bytes) { 155 + Ok(c) => c, 156 + Err(e) => { 157 + error!("Failed to parse commit: {:?}", e); 158 + return ( 159 + StatusCode::INTERNAL_SERVER_ERROR, 160 + Json(json!({"error": "InternalError"})), 161 + ) 162 + .into_response(); 163 + } 164 + }; 165 + 166 + let mst_root = commit.data; 167 + let store = Arc::new(state.block_store.clone()); 168 + let mst = Mst::load(store.clone(), mst_root, None); 169 + 170 + let collection_nsid = match input.collection.parse::<Nsid>() { 171 + Ok(n) => n, 172 + Err(_) => { 173 + return ( 174 + StatusCode::BAD_REQUEST, 175 + Json(json!({"error": "InvalidCollection"})), 176 + ) 177 + .into_response(); 178 + } 179 + }; 180 + 181 + let rkey = input 182 + .rkey 183 + .unwrap_or_else(|| Utc::now().format("%Y%m%d%H%M%S%f").to_string()); 184 + 185 + let mut record_bytes = Vec::new(); 186 + if let Err(e) = serde_ipld_dagcbor::to_writer(&mut record_bytes, &input.record) { 187 + error!("Error serializing record: {:?}", e); 188 + return ( 189 + StatusCode::BAD_REQUEST, 190 + Json(json!({"error": "InvalidRecord", "message": "Failed to serialize record"})), 191 + ) 192 + .into_response(); 193 + } 194 + 195 + let record_cid = match state.block_store.put(&record_bytes).await { 196 + Ok(c) => c, 197 + Err(e) => { 198 + error!("Failed to save record block: {:?}", e); 199 + return ( 200 + StatusCode::INTERNAL_SERVER_ERROR, 201 + Json(json!({"error": "InternalError"})), 202 + ) 203 + .into_response(); 204 + } 205 + }; 206 + 207 + let key = format!("{}/{}", collection_nsid, rkey); 208 + if let Err(e) = mst.update(&key, record_cid).await { 209 + error!("Failed to update MST: {:?}", e); 210 + return ( 211 + StatusCode::INTERNAL_SERVER_ERROR, 212 + Json(json!({"error": "InternalError"})), 213 + ) 214 + .into_response(); 215 + } 216 + 217 + let new_mst_root = match mst.root().await { 218 + Ok(c) => c, 219 + Err(e) => { 220 + error!("Failed to get new MST root: {:?}", e); 221 + return ( 222 + StatusCode::INTERNAL_SERVER_ERROR, 223 + Json(json!({"error": "InternalError"})), 224 + ) 225 + .into_response(); 226 + } 227 + }; 228 + 229 + let did_obj = match Did::new(&did) { 230 + Ok(d) => d, 231 + Err(_) => { 232 + return ( 233 + StatusCode::INTERNAL_SERVER_ERROR, 234 + Json(json!({"error": "InternalError", "message": "Invalid DID"})), 235 + ) 236 + .into_response(); 237 + } 238 + }; 239 + 240 + let rev = Tid::now(LimitedU32::MIN); 241 + 242 + let new_commit = Commit::new_unsigned(did_obj, new_mst_root, rev, Some(current_root_cid)); 243 + 244 + let new_commit_bytes = match new_commit.to_cbor() { 245 + Ok(b) => b, 246 + Err(e) => { 247 + error!("Failed to serialize new commit: {:?}", e); 248 + return ( 249 + StatusCode::INTERNAL_SERVER_ERROR, 250 + Json(json!({"error": "InternalError"})), 251 + ) 252 + .into_response(); 253 + } 254 + }; 255 + 256 + let new_root_cid = match state.block_store.put(&new_commit_bytes).await { 257 + Ok(c) => c, 258 + Err(e) => { 259 + error!("Failed to save new commit: {:?}", e); 260 + return ( 261 + StatusCode::INTERNAL_SERVER_ERROR, 262 + Json(json!({"error": "InternalError"})), 263 + ) 264 + .into_response(); 265 + } 266 + }; 267 + 268 + let update_repo = sqlx::query("UPDATE repos SET repo_root_cid = $1 WHERE user_id = $2") 269 + .bind(new_root_cid.to_string()) 270 + .bind(user_id) 271 + .execute(&state.db) 272 + .await; 273 + 274 + if let Err(e) = update_repo { 275 + error!("Failed to update repo root in DB: {:?}", e); 276 + return ( 277 + StatusCode::INTERNAL_SERVER_ERROR, 278 + Json(json!({"error": "InternalError"})), 279 + ) 280 + .into_response(); 281 + } 282 + 283 + let record_insert = sqlx::query( 284 + "INSERT INTO records (repo_id, collection, rkey, record_cid) VALUES ($1, $2, $3, $4) 285 + ON CONFLICT (repo_id, collection, rkey) DO UPDATE SET record_cid = $4, created_at = NOW()", 286 + ) 287 + .bind(user_id) 288 + .bind(&input.collection) 289 + .bind(&rkey) 290 + .bind(record_cid.to_string()) 291 + .execute(&state.db) 292 + .await; 293 + 294 + if let Err(e) = record_insert { 295 + error!("Error inserting record index: {:?}", e); 296 + return ( 297 + StatusCode::INTERNAL_SERVER_ERROR, 298 + Json(json!({"error": "InternalError", "message": "Failed to index record"})), 299 + ) 300 + .into_response(); 301 + } 302 + 303 + let output = CreateRecordOutput { 304 + uri: format!("at://{}/{}/{}", input.repo, input.collection, rkey), 305 + cid: record_cid.to_string(), 306 + }; 307 + (StatusCode::OK, Json(output)).into_response() 308 + } 309 + 310 + #[derive(Deserialize)] 311 + #[allow(dead_code)] 312 + pub struct PutRecordInput { 313 + pub repo: String, 314 + pub collection: String, 315 + pub rkey: String, 316 + pub validate: Option<bool>, 317 + pub record: serde_json::Value, 318 + #[serde(rename = "swapCommit")] 319 + pub swap_commit: Option<String>, 320 + } 321 + 322 + #[derive(Serialize)] 323 + #[serde(rename_all = "camelCase")] 324 + pub struct PutRecordOutput { 325 + pub uri: String, 326 + pub cid: String, 327 + } 328 + 329 + pub async fn put_record( 330 + State(state): State<AppState>, 331 + headers: axum::http::HeaderMap, 332 + Json(input): Json<PutRecordInput>, 333 + ) -> Response { 334 + let auth_header = headers.get("Authorization"); 335 + if auth_header.is_none() { 336 + return ( 337 + StatusCode::UNAUTHORIZED, 338 + Json(json!({"error": "AuthenticationRequired"})), 339 + ) 340 + .into_response(); 341 + } 342 + let token = auth_header 343 + .unwrap() 344 + .to_str() 345 + .unwrap_or("") 346 + .replace("Bearer ", ""); 347 + 348 + let session = sqlx::query( 349 + "SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.access_jwt = $1" 350 + ) 351 + .bind(&token) 352 + .fetch_optional(&state.db) 353 + .await 354 + .unwrap_or(None); 355 + 356 + let (did, key_bytes) = match session { 357 + Some(row) => ( 358 + row.get::<String, _>("did"), 359 + row.get::<Vec<u8>, _>("key_bytes"), 360 + ), 361 + None => { 362 + return ( 363 + StatusCode::UNAUTHORIZED, 364 + Json(json!({"error": "AuthenticationFailed"})), 365 + ) 366 + .into_response(); 367 + } 368 + }; 369 + 370 + if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 371 + return ( 372 + StatusCode::UNAUTHORIZED, 373 + Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})), 374 + ) 375 + .into_response(); 376 + } 377 + 378 + if input.repo != did { 379 + return (StatusCode::FORBIDDEN, Json(json!({"error": "InvalidRepo", "message": "Repo does not match authenticated user"}))).into_response(); 380 + } 381 + 382 + let user_query = sqlx::query("SELECT id FROM users WHERE did = $1") 383 + .bind(&did) 384 + .fetch_optional(&state.db) 385 + .await; 386 + 387 + let user_id: uuid::Uuid = match user_query { 388 + Ok(Some(row)) => row.get("id"), 389 + _ => { 390 + return ( 391 + StatusCode::INTERNAL_SERVER_ERROR, 392 + Json(json!({"error": "InternalError", "message": "User not found"})), 393 + ) 394 + .into_response(); 395 + } 396 + }; 397 + 398 + let repo_root_query = sqlx::query("SELECT repo_root_cid FROM repos WHERE user_id = $1") 399 + .bind(user_id) 400 + .fetch_optional(&state.db) 401 + .await; 402 + 403 + let current_root_cid = match repo_root_query { 404 + Ok(Some(row)) => { 405 + let cid_str: String = row.get("repo_root_cid"); 406 + Cid::from_str(&cid_str).ok() 407 + } 408 + _ => None, 409 + }; 410 + 411 + if current_root_cid.is_none() { 412 + error!("Repo root not found for user {}", did); 413 + return ( 414 + StatusCode::INTERNAL_SERVER_ERROR, 415 + Json(json!({"error": "InternalError", "message": "Repo root not found"})), 416 + ) 417 + .into_response(); 418 + } 419 + let current_root_cid = current_root_cid.unwrap(); 420 + 421 + let commit_bytes = match state.block_store.get(&current_root_cid).await { 422 + Ok(Some(b)) => b, 423 + Ok(None) => { 424 + error!("Commit block not found: {}", current_root_cid); 425 + return ( 426 + StatusCode::INTERNAL_SERVER_ERROR, 427 + Json(json!({"error": "InternalError", "message": "Commit block not found"})), 428 + ) 429 + .into_response(); 430 + } 431 + Err(e) => { 432 + error!("Failed to load commit block: {:?}", e); 433 + return ( 434 + StatusCode::INTERNAL_SERVER_ERROR, 435 + Json(json!({"error": "InternalError", "message": "Failed to load commit block"})), 436 + ) 437 + .into_response(); 438 + } 439 + }; 440 + 441 + let commit = match Commit::from_cbor(&commit_bytes) { 442 + Ok(c) => c, 443 + Err(e) => { 444 + error!("Failed to parse commit: {:?}", e); 445 + return ( 446 + StatusCode::INTERNAL_SERVER_ERROR, 447 + Json(json!({"error": "InternalError", "message": "Failed to parse commit"})), 448 + ) 449 + .into_response(); 450 + } 451 + }; 452 + 453 + let mst_root = commit.data; 454 + let store = Arc::new(state.block_store.clone()); 455 + let mst = Mst::load(store.clone(), mst_root, None); 456 + 457 + let collection_nsid = match input.collection.parse::<Nsid>() { 458 + Ok(n) => n, 459 + Err(_) => { 460 + return ( 461 + StatusCode::BAD_REQUEST, 462 + Json(json!({"error": "InvalidCollection"})), 463 + ) 464 + .into_response(); 465 + } 466 + }; 467 + 468 + let rkey = input.rkey.clone(); 469 + 470 + let mut record_bytes = Vec::new(); 471 + if let Err(e) = serde_ipld_dagcbor::to_writer(&mut record_bytes, &input.record) { 472 + error!("Error serializing record: {:?}", e); 473 + return ( 474 + StatusCode::BAD_REQUEST, 475 + Json(json!({"error": "InvalidRecord", "message": "Failed to serialize record"})), 476 + ) 477 + .into_response(); 478 + } 479 + 480 + let record_cid = match state.block_store.put(&record_bytes).await { 481 + Ok(c) => c, 482 + Err(e) => { 483 + error!("Failed to save record block: {:?}", e); 484 + return ( 485 + StatusCode::INTERNAL_SERVER_ERROR, 486 + Json(json!({"error": "InternalError", "message": "Failed to save record block"})), 487 + ) 488 + .into_response(); 489 + } 490 + }; 491 + 492 + let key = format!("{}/{}", collection_nsid, rkey); 493 + if let Err(e) = mst.update(&key, record_cid).await { 494 + error!("Failed to update MST: {:?}", e); 495 + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": format!("Failed to update MST: {:?}", e)}))).into_response(); 496 + } 497 + 498 + let new_mst_root = match mst.root().await { 499 + Ok(c) => c, 500 + Err(e) => { 501 + error!("Failed to get new MST root: {:?}", e); 502 + return ( 503 + StatusCode::INTERNAL_SERVER_ERROR, 504 + Json(json!({"error": "InternalError", "message": "Failed to get new MST root"})), 505 + ) 506 + .into_response(); 507 + } 508 + }; 509 + 510 + let did_obj = match Did::new(&did) { 511 + Ok(d) => d, 512 + Err(_) => { 513 + return ( 514 + StatusCode::INTERNAL_SERVER_ERROR, 515 + Json(json!({"error": "InternalError", "message": "Invalid DID"})), 516 + ) 517 + .into_response(); 518 + } 519 + }; 520 + 521 + let rev = Tid::now(LimitedU32::MIN); 522 + 523 + let new_commit = Commit::new_unsigned(did_obj, new_mst_root, rev, Some(current_root_cid)); 524 + 525 + let new_commit_bytes = match new_commit.to_cbor() { 526 + Ok(b) => b, 527 + Err(e) => { 528 + error!("Failed to serialize new commit: {:?}", e); 529 + return ( 530 + StatusCode::INTERNAL_SERVER_ERROR, 531 + Json( 532 + json!({"error": "InternalError", "message": "Failed to serialize new commit"}), 533 + ), 534 + ) 535 + .into_response(); 536 + } 537 + }; 538 + 539 + let new_root_cid = match state.block_store.put(&new_commit_bytes).await { 540 + Ok(c) => c, 541 + Err(e) => { 542 + error!("Failed to save new commit: {:?}", e); 543 + return ( 544 + StatusCode::INTERNAL_SERVER_ERROR, 545 + Json(json!({"error": "InternalError", "message": "Failed to save new commit"})), 546 + ) 547 + .into_response(); 548 + } 549 + }; 550 + 551 + let update_repo = sqlx::query("UPDATE repos SET repo_root_cid = $1 WHERE user_id = $2") 552 + .bind(new_root_cid.to_string()) 553 + .bind(user_id) 554 + .execute(&state.db) 555 + .await; 556 + 557 + if let Err(e) = update_repo { 558 + error!("Failed to update repo root in DB: {:?}", e); 559 + return ( 560 + StatusCode::INTERNAL_SERVER_ERROR, 561 + Json(json!({"error": "InternalError", "message": "Failed to update repo root in DB"})), 562 + ) 563 + .into_response(); 564 + } 565 + 566 + let record_insert = sqlx::query( 567 + "INSERT INTO records (repo_id, collection, rkey, record_cid) VALUES ($1, $2, $3, $4) 568 + ON CONFLICT (repo_id, collection, rkey) DO UPDATE SET record_cid = $4, created_at = NOW()", 569 + ) 570 + .bind(user_id) 571 + .bind(&input.collection) 572 + .bind(&rkey) 573 + .bind(record_cid.to_string()) 574 + .execute(&state.db) 575 + .await; 576 + 577 + if let Err(e) = record_insert { 578 + error!("Error inserting record index: {:?}", e); 579 + return ( 580 + StatusCode::INTERNAL_SERVER_ERROR, 581 + Json(json!({"error": "InternalError", "message": "Failed to index record"})), 582 + ) 583 + .into_response(); 584 + } 585 + 586 + let output = PutRecordOutput { 587 + uri: format!("at://{}/{}/{}", input.repo, input.collection, rkey), 588 + cid: record_cid.to_string(), 589 + }; 590 + (StatusCode::OK, Json(output)).into_response() 591 + }
-294
src/api/server.rs
··· 1 - use axum::{ 2 - extract::State, 3 - Json, 4 - response::{IntoResponse, Response}, 5 - http::StatusCode, 6 - }; 7 - use serde::{Deserialize, Serialize}; 8 - use serde_json::json; 9 - use crate::state::AppState; 10 - use sqlx::Row; 11 - use bcrypt::verify; 12 - use tracing::{info, error, warn}; 13 - 14 - pub async fn describe_server() -> impl IntoResponse { 15 - let domains_str = std::env::var("AVAILABLE_USER_DOMAINS").unwrap_or_else(|_| "example.com".to_string()); 16 - let domains: Vec<&str> = domains_str.split(',').map(|s| s.trim()).collect(); 17 - 18 - Json(json!({ 19 - "availableUserDomains": domains 20 - })) 21 - } 22 - 23 - pub async fn health(State(state): State<AppState>) -> impl IntoResponse { 24 - match sqlx::query("SELECT 1").execute(&state.db).await { 25 - Ok(_) => (StatusCode::OK, "OK"), 26 - Err(e) => { 27 - error!("Health check failed: {:?}", e); 28 - (StatusCode::SERVICE_UNAVAILABLE, "Service Unavailable") 29 - } 30 - } 31 - } 32 - 33 - #[derive(Deserialize)] 34 - pub struct CreateSessionInput { 35 - pub identifier: String, 36 - pub password: String, 37 - } 38 - 39 - #[derive(Serialize)] 40 - #[serde(rename_all = "camelCase")] 41 - pub struct CreateSessionOutput { 42 - pub access_jwt: String, 43 - pub refresh_jwt: String, 44 - pub handle: String, 45 - pub did: String, 46 - } 47 - 48 - pub async fn create_session( 49 - State(state): State<AppState>, 50 - Json(input): Json<CreateSessionInput>, 51 - ) -> Response { 52 - info!("create_session: identifier='{}'", input.identifier); 53 - 54 - let user_row = sqlx::query("SELECT u.did, u.handle, u.password_hash, k.key_bytes FROM users u JOIN user_keys k ON u.id = k.user_id WHERE u.handle = $1 OR u.email = $1") 55 - .bind(&input.identifier) 56 - .fetch_optional(&state.db) 57 - .await; 58 - 59 - match user_row { 60 - Ok(Some(row)) => { 61 - let stored_hash: String = row.get("password_hash"); 62 - 63 - if verify(&input.password, &stored_hash).unwrap_or(false) { 64 - let did: String = row.get("did"); 65 - let handle: String = row.get("handle"); 66 - let key_bytes: Vec<u8> = row.get("key_bytes"); 67 - 68 - let access_jwt = match crate::auth::create_access_token(&did, &key_bytes) { 69 - Ok(t) => t, 70 - Err(e) => { 71 - error!("Failed to create access token: {:?}", e); 72 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 73 - } 74 - }; 75 - 76 - let refresh_jwt = match crate::auth::create_refresh_token(&did, &key_bytes) { 77 - Ok(t) => t, 78 - Err(e) => { 79 - error!("Failed to create refresh token: {:?}", e); 80 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 81 - } 82 - }; 83 - 84 - let session_insert = sqlx::query("INSERT INTO sessions (access_jwt, refresh_jwt, did) VALUES ($1, $2, $3)") 85 - .bind(&access_jwt) 86 - .bind(&refresh_jwt) 87 - .bind(&did) 88 - .execute(&state.db) 89 - .await; 90 - 91 - match session_insert { 92 - Ok(_) => { 93 - return (StatusCode::OK, Json(CreateSessionOutput { 94 - access_jwt, 95 - refresh_jwt, 96 - handle, 97 - did, 98 - })).into_response(); 99 - }, 100 - Err(e) => { 101 - error!("Failed to insert session: {:?}", e); 102 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 103 - } 104 - } 105 - } else { 106 - warn!("Password verification failed for identifier: {}", input.identifier); 107 - } 108 - }, 109 - Ok(None) => { 110 - warn!("User not found for identifier: {}", input.identifier); 111 - }, 112 - Err(e) => { 113 - error!("Database error fetching user: {:?}", e); 114 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 115 - } 116 - } 117 - 118 - (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid identifier or password"}))).into_response() 119 - } 120 - 121 - pub async fn get_session( 122 - State(state): State<AppState>, 123 - headers: axum::http::HeaderMap, 124 - ) -> Response { 125 - let auth_header = headers.get("Authorization"); 126 - if auth_header.is_none() { 127 - return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationRequired"}))).into_response(); 128 - } 129 - 130 - let token = auth_header.unwrap().to_str().unwrap_or("").replace("Bearer ", ""); 131 - 132 - let result = sqlx::query( 133 - r#" 134 - SELECT u.handle, u.did, u.email, k.key_bytes 135 - FROM sessions s 136 - JOIN users u ON s.did = u.did 137 - JOIN user_keys k ON u.id = k.user_id 138 - WHERE s.access_jwt = $1 139 - "# 140 - ) 141 - .bind(&token) 142 - .fetch_optional(&state.db) 143 - .await; 144 - 145 - match result { 146 - Ok(Some(row)) => { 147 - let handle: String = row.get("handle"); 148 - let did: String = row.get("did"); 149 - let email: String = row.get("email"); 150 - let key_bytes: Vec<u8> = row.get("key_bytes"); 151 - 152 - if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 153 - return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"}))).into_response(); 154 - } 155 - 156 - return (StatusCode::OK, Json(json!({ 157 - "handle": handle, 158 - "did": did, 159 - "email": email, 160 - "didDoc": {} 161 - }))).into_response(); 162 - }, 163 - Ok(None) => { 164 - return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed"}))).into_response(); 165 - }, 166 - Err(e) => { 167 - error!("Database error in get_session: {:?}", e); 168 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 169 - } 170 - } 171 - } 172 - 173 - pub async fn delete_session( 174 - State(state): State<AppState>, 175 - headers: axum::http::HeaderMap, 176 - ) -> Response { 177 - let auth_header = headers.get("Authorization"); 178 - if auth_header.is_none() { 179 - return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationRequired"}))).into_response(); 180 - } 181 - 182 - let token = auth_header.unwrap().to_str().unwrap_or("").replace("Bearer ", ""); 183 - 184 - let result = sqlx::query("DELETE FROM sessions WHERE access_jwt = $1") 185 - .bind(token) 186 - .execute(&state.db) 187 - .await; 188 - 189 - match result { 190 - Ok(res) => { 191 - if res.rows_affected() > 0 { 192 - return (StatusCode::OK, Json(json!({}))).into_response(); 193 - } 194 - }, 195 - Err(e) => { 196 - error!("Database error in delete_session: {:?}", e); 197 - } 198 - } 199 - 200 - (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed"}))).into_response() 201 - } 202 - 203 - pub async fn refresh_session( 204 - State(state): State<AppState>, 205 - headers: axum::http::HeaderMap, 206 - ) -> Response { 207 - let auth_header = headers.get("Authorization"); 208 - if auth_header.is_none() { 209 - return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationRequired"}))).into_response(); 210 - } 211 - 212 - let refresh_token = auth_header.unwrap().to_str().unwrap_or("").replace("Bearer ", ""); 213 - 214 - let session = sqlx::query( 215 - "SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.refresh_jwt = $1" 216 - ) 217 - .bind(&refresh_token) 218 - .fetch_optional(&state.db) 219 - .await; 220 - 221 - match session { 222 - Ok(Some(session_row)) => { 223 - let did: String = session_row.get("did"); 224 - let key_bytes: Vec<u8> = session_row.get("key_bytes"); 225 - 226 - if let Err(_) = crate::auth::verify_token(&refresh_token, &key_bytes) { 227 - return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid refresh token signature"}))).into_response(); 228 - } 229 - 230 - let new_access_jwt = match crate::auth::create_access_token(&did, &key_bytes) { 231 - Ok(t) => t, 232 - Err(e) => { 233 - error!("Failed to create access token: {:?}", e); 234 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 235 - } 236 - }; 237 - let new_refresh_jwt = match crate::auth::create_refresh_token(&did, &key_bytes) { 238 - Ok(t) => t, 239 - Err(e) => { 240 - error!("Failed to create refresh token: {:?}", e); 241 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 242 - } 243 - }; 244 - 245 - let update = sqlx::query("UPDATE sessions SET access_jwt = $1, refresh_jwt = $2 WHERE refresh_jwt = $3") 246 - .bind(&new_access_jwt) 247 - .bind(&new_refresh_jwt) 248 - .bind(&refresh_token) 249 - .execute(&state.db) 250 - .await; 251 - 252 - match update { 253 - Ok(_) => { 254 - let user = sqlx::query("SELECT handle FROM users WHERE did = $1") 255 - .bind(&did) 256 - .fetch_optional(&state.db) 257 - .await; 258 - 259 - match user { 260 - Ok(Some(u)) => { 261 - let handle: String = u.get("handle"); 262 - return (StatusCode::OK, Json(json!({ 263 - "accessJwt": new_access_jwt, 264 - "refreshJwt": new_refresh_jwt, 265 - "handle": handle, 266 - "did": did 267 - }))).into_response(); 268 - }, 269 - Ok(None) => { 270 - error!("User not found for existing session: {}", did); 271 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 272 - }, 273 - Err(e) => { 274 - error!("Database error fetching user: {:?}", e); 275 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 276 - } 277 - } 278 - }, 279 - Err(e) => { 280 - error!("Database error updating session: {:?}", e); 281 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 282 - } 283 - } 284 - }, 285 - Ok(None) => { 286 - return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid refresh token"}))).into_response(); 287 - }, 288 - Err(e) => { 289 - error!("Database error fetching session: {:?}", e); 290 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); 291 - } 292 - } 293 - } 294 -
···
+25
src/api/server/meta.rs
···
··· 1 + use crate::state::AppState; 2 + use axum::{Json, extract::State, http::StatusCode, response::IntoResponse}; 3 + use serde_json::json; 4 + 5 + use tracing::error; 6 + 7 + pub async fn describe_server() -> impl IntoResponse { 8 + let domains_str = 9 + std::env::var("AVAILABLE_USER_DOMAINS").unwrap_or_else(|_| "example.com".to_string()); 10 + let domains: Vec<&str> = domains_str.split(',').map(|s| s.trim()).collect(); 11 + 12 + Json(json!({ 13 + "availableUserDomains": domains 14 + })) 15 + } 16 + 17 + pub async fn health(State(state): State<AppState>) -> impl IntoResponse { 18 + match sqlx::query("SELECT 1").execute(&state.db).await { 19 + Ok(_) => (StatusCode::OK, "OK"), 20 + Err(e) => { 21 + error!("Health check failed: {:?}", e); 22 + (StatusCode::SERVICE_UNAVAILABLE, "Service Unavailable") 23 + } 24 + } 25 + }
+5
src/api/server/mod.rs
···
··· 1 + pub mod meta; 2 + pub mod session; 3 + 4 + pub use meta::{describe_server, health}; 5 + pub use session::{create_session, delete_session, get_session, refresh_session};
+377
src/api/server/session.rs
···
··· 1 + use crate::state::AppState; 2 + use axum::{ 3 + Json, 4 + extract::State, 5 + http::StatusCode, 6 + response::{IntoResponse, Response}, 7 + }; 8 + use bcrypt::verify; 9 + use serde::{Deserialize, Serialize}; 10 + use serde_json::json; 11 + use sqlx::Row; 12 + use tracing::{error, info, warn}; 13 + 14 + #[derive(Deserialize)] 15 + pub struct CreateSessionInput { 16 + pub identifier: String, 17 + pub password: String, 18 + } 19 + 20 + #[derive(Serialize)] 21 + #[serde(rename_all = "camelCase")] 22 + pub struct CreateSessionOutput { 23 + pub access_jwt: String, 24 + pub refresh_jwt: String, 25 + pub handle: String, 26 + pub did: String, 27 + } 28 + 29 + pub async fn create_session( 30 + State(state): State<AppState>, 31 + Json(input): Json<CreateSessionInput>, 32 + ) -> Response { 33 + info!("create_session: identifier='{}'", input.identifier); 34 + 35 + let user_row = sqlx::query("SELECT u.did, u.handle, u.password_hash, k.key_bytes FROM users u JOIN user_keys k ON u.id = k.user_id WHERE u.handle = $1 OR u.email = $1") 36 + .bind(&input.identifier) 37 + .fetch_optional(&state.db) 38 + .await; 39 + 40 + match user_row { 41 + Ok(Some(row)) => { 42 + let stored_hash: String = row.get("password_hash"); 43 + 44 + if verify(&input.password, &stored_hash).unwrap_or(false) { 45 + let did: String = row.get("did"); 46 + let handle: String = row.get("handle"); 47 + let key_bytes: Vec<u8> = row.get("key_bytes"); 48 + 49 + let access_jwt = match crate::auth::create_access_token(&did, &key_bytes) { 50 + Ok(t) => t, 51 + Err(e) => { 52 + error!("Failed to create access token: {:?}", e); 53 + return ( 54 + StatusCode::INTERNAL_SERVER_ERROR, 55 + Json(json!({"error": "InternalError"})), 56 + ) 57 + .into_response(); 58 + } 59 + }; 60 + 61 + let refresh_jwt = match crate::auth::create_refresh_token(&did, &key_bytes) { 62 + Ok(t) => t, 63 + Err(e) => { 64 + error!("Failed to create refresh token: {:?}", e); 65 + return ( 66 + StatusCode::INTERNAL_SERVER_ERROR, 67 + Json(json!({"error": "InternalError"})), 68 + ) 69 + .into_response(); 70 + } 71 + }; 72 + 73 + let session_insert = sqlx::query( 74 + "INSERT INTO sessions (access_jwt, refresh_jwt, did) VALUES ($1, $2, $3)", 75 + ) 76 + .bind(&access_jwt) 77 + .bind(&refresh_jwt) 78 + .bind(&did) 79 + .execute(&state.db) 80 + .await; 81 + 82 + match session_insert { 83 + Ok(_) => { 84 + return ( 85 + StatusCode::OK, 86 + Json(CreateSessionOutput { 87 + access_jwt, 88 + refresh_jwt, 89 + handle, 90 + did, 91 + }), 92 + ) 93 + .into_response(); 94 + } 95 + Err(e) => { 96 + error!("Failed to insert session: {:?}", e); 97 + return ( 98 + StatusCode::INTERNAL_SERVER_ERROR, 99 + Json(json!({"error": "InternalError"})), 100 + ) 101 + .into_response(); 102 + } 103 + } 104 + } else { 105 + warn!( 106 + "Password verification failed for identifier: {}", 107 + input.identifier 108 + ); 109 + } 110 + } 111 + Ok(None) => { 112 + warn!("User not found for identifier: {}", input.identifier); 113 + } 114 + Err(e) => { 115 + error!("Database error fetching user: {:?}", e); 116 + return ( 117 + StatusCode::INTERNAL_SERVER_ERROR, 118 + Json(json!({"error": "InternalError"})), 119 + ) 120 + .into_response(); 121 + } 122 + } 123 + 124 + ( 125 + StatusCode::UNAUTHORIZED, 126 + Json(json!({"error": "AuthenticationFailed", "message": "Invalid identifier or password"})), 127 + ) 128 + .into_response() 129 + } 130 + 131 + pub async fn get_session( 132 + State(state): State<AppState>, 133 + headers: axum::http::HeaderMap, 134 + ) -> Response { 135 + let auth_header = headers.get("Authorization"); 136 + if auth_header.is_none() { 137 + return ( 138 + StatusCode::UNAUTHORIZED, 139 + Json(json!({"error": "AuthenticationRequired"})), 140 + ) 141 + .into_response(); 142 + } 143 + 144 + let token = auth_header 145 + .unwrap() 146 + .to_str() 147 + .unwrap_or("") 148 + .replace("Bearer ", ""); 149 + 150 + let result = sqlx::query( 151 + r#" 152 + SELECT u.handle, u.did, u.email, k.key_bytes 153 + FROM sessions s 154 + JOIN users u ON s.did = u.did 155 + JOIN user_keys k ON u.id = k.user_id 156 + WHERE s.access_jwt = $1 157 + "#, 158 + ) 159 + .bind(&token) 160 + .fetch_optional(&state.db) 161 + .await; 162 + 163 + match result { 164 + Ok(Some(row)) => { 165 + let handle: String = row.get("handle"); 166 + let did: String = row.get("did"); 167 + let email: String = row.get("email"); 168 + let key_bytes: Vec<u8> = row.get("key_bytes"); 169 + 170 + if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 171 + return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"}))).into_response(); 172 + } 173 + 174 + return ( 175 + StatusCode::OK, 176 + Json(json!({ 177 + "handle": handle, 178 + "did": did, 179 + "email": email, 180 + "didDoc": {} 181 + })), 182 + ) 183 + .into_response(); 184 + } 185 + Ok(None) => { 186 + return ( 187 + StatusCode::UNAUTHORIZED, 188 + Json(json!({"error": "AuthenticationFailed"})), 189 + ) 190 + .into_response(); 191 + } 192 + Err(e) => { 193 + error!("Database error in get_session: {:?}", e); 194 + return ( 195 + StatusCode::INTERNAL_SERVER_ERROR, 196 + Json(json!({"error": "InternalError"})), 197 + ) 198 + .into_response(); 199 + } 200 + } 201 + } 202 + 203 + pub async fn delete_session( 204 + State(state): State<AppState>, 205 + headers: axum::http::HeaderMap, 206 + ) -> Response { 207 + let auth_header = headers.get("Authorization"); 208 + if auth_header.is_none() { 209 + return ( 210 + StatusCode::UNAUTHORIZED, 211 + Json(json!({"error": "AuthenticationRequired"})), 212 + ) 213 + .into_response(); 214 + } 215 + 216 + let token = auth_header 217 + .unwrap() 218 + .to_str() 219 + .unwrap_or("") 220 + .replace("Bearer ", ""); 221 + 222 + let result = sqlx::query("DELETE FROM sessions WHERE access_jwt = $1") 223 + .bind(token) 224 + .execute(&state.db) 225 + .await; 226 + 227 + match result { 228 + Ok(res) => { 229 + if res.rows_affected() > 0 { 230 + return (StatusCode::OK, Json(json!({}))).into_response(); 231 + } 232 + } 233 + Err(e) => { 234 + error!("Database error in delete_session: {:?}", e); 235 + } 236 + } 237 + 238 + ( 239 + StatusCode::UNAUTHORIZED, 240 + Json(json!({"error": "AuthenticationFailed"})), 241 + ) 242 + .into_response() 243 + } 244 + 245 + pub async fn refresh_session( 246 + State(state): State<AppState>, 247 + headers: axum::http::HeaderMap, 248 + ) -> Response { 249 + let auth_header = headers.get("Authorization"); 250 + if auth_header.is_none() { 251 + return ( 252 + StatusCode::UNAUTHORIZED, 253 + Json(json!({"error": "AuthenticationRequired"})), 254 + ) 255 + .into_response(); 256 + } 257 + 258 + let refresh_token = auth_header 259 + .unwrap() 260 + .to_str() 261 + .unwrap_or("") 262 + .replace("Bearer ", ""); 263 + 264 + let session = sqlx::query( 265 + "SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.refresh_jwt = $1" 266 + ) 267 + .bind(&refresh_token) 268 + .fetch_optional(&state.db) 269 + .await; 270 + 271 + match session { 272 + Ok(Some(session_row)) => { 273 + let did: String = session_row.get("did"); 274 + let key_bytes: Vec<u8> = session_row.get("key_bytes"); 275 + 276 + if let Err(_) = crate::auth::verify_token(&refresh_token, &key_bytes) { 277 + return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid refresh token signature"}))).into_response(); 278 + } 279 + 280 + let new_access_jwt = match crate::auth::create_access_token(&did, &key_bytes) { 281 + Ok(t) => t, 282 + Err(e) => { 283 + error!("Failed to create access token: {:?}", e); 284 + return ( 285 + StatusCode::INTERNAL_SERVER_ERROR, 286 + Json(json!({"error": "InternalError"})), 287 + ) 288 + .into_response(); 289 + } 290 + }; 291 + let new_refresh_jwt = match crate::auth::create_refresh_token(&did, &key_bytes) { 292 + Ok(t) => t, 293 + Err(e) => { 294 + error!("Failed to create refresh token: {:?}", e); 295 + return ( 296 + StatusCode::INTERNAL_SERVER_ERROR, 297 + Json(json!({"error": "InternalError"})), 298 + ) 299 + .into_response(); 300 + } 301 + }; 302 + 303 + let update = sqlx::query( 304 + "UPDATE sessions SET access_jwt = $1, refresh_jwt = $2 WHERE refresh_jwt = $3", 305 + ) 306 + .bind(&new_access_jwt) 307 + .bind(&new_refresh_jwt) 308 + .bind(&refresh_token) 309 + .execute(&state.db) 310 + .await; 311 + 312 + match update { 313 + Ok(_) => { 314 + let user = sqlx::query("SELECT handle FROM users WHERE did = $1") 315 + .bind(&did) 316 + .fetch_optional(&state.db) 317 + .await; 318 + 319 + match user { 320 + Ok(Some(u)) => { 321 + let handle: String = u.get("handle"); 322 + return ( 323 + StatusCode::OK, 324 + Json(json!({ 325 + "accessJwt": new_access_jwt, 326 + "refreshJwt": new_refresh_jwt, 327 + "handle": handle, 328 + "did": did 329 + })), 330 + ) 331 + .into_response(); 332 + } 333 + Ok(None) => { 334 + error!("User not found for existing session: {}", did); 335 + return ( 336 + StatusCode::INTERNAL_SERVER_ERROR, 337 + Json(json!({"error": "InternalError"})), 338 + ) 339 + .into_response(); 340 + } 341 + Err(e) => { 342 + error!("Database error fetching user: {:?}", e); 343 + return ( 344 + StatusCode::INTERNAL_SERVER_ERROR, 345 + Json(json!({"error": "InternalError"})), 346 + ) 347 + .into_response(); 348 + } 349 + } 350 + } 351 + Err(e) => { 352 + error!("Database error updating session: {:?}", e); 353 + return ( 354 + StatusCode::INTERNAL_SERVER_ERROR, 355 + Json(json!({"error": "InternalError"})), 356 + ) 357 + .into_response(); 358 + } 359 + } 360 + } 361 + Ok(None) => { 362 + return ( 363 + StatusCode::UNAUTHORIZED, 364 + Json(json!({"error": "AuthenticationFailed", "message": "Invalid refresh token"})), 365 + ) 366 + .into_response(); 367 + } 368 + Err(e) => { 369 + error!("Database error fetching session: {:?}", e); 370 + return ( 371 + StatusCode::INTERNAL_SERVER_ERROR, 372 + Json(json!({"error": "InternalError"})), 373 + ) 374 + .into_response(); 375 + } 376 + } 377 + }
-157
src/auth.rs
··· 1 - use serde::{Deserialize, Serialize}; 2 - use chrono::{Utc, Duration}; 3 - use k256::ecdsa::{SigningKey, VerifyingKey, signature::Signer, signature::Verifier, Signature}; 4 - use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 5 - use anyhow::{Context, Result, anyhow}; 6 - 7 - #[derive(Debug, Serialize, Deserialize)] 8 - pub struct Claims { 9 - pub iss: String, 10 - pub sub: String, 11 - pub aud: String, 12 - pub exp: usize, 13 - pub iat: usize, 14 - #[serde(skip_serializing_if = "Option::is_none")] 15 - pub scope: Option<String>, 16 - #[serde(skip_serializing_if = "Option::is_none")] 17 - pub lxm: Option<String>, 18 - pub jti: String, 19 - } 20 - 21 - #[derive(Debug, Serialize, Deserialize)] 22 - struct Header { 23 - alg: String, 24 - typ: String, 25 - } 26 - 27 - #[derive(Debug, Serialize, Deserialize)] 28 - struct UnsafeClaims { 29 - iss: String, 30 - sub: Option<String>, 31 - } 32 - 33 - // fancy boy TokenData equivalent for compatibility/structure 34 - pub struct TokenData<T> { 35 - pub claims: T, 36 - } 37 - 38 - pub fn get_did_from_token(token: &str) -> Result<String, String> { 39 - let parts: Vec<&str> = token.split('.').collect(); 40 - if parts.len() != 3 { 41 - return Err("Invalid token format".to_string()); 42 - } 43 - 44 - let payload_bytes = URL_SAFE_NO_PAD.decode(parts[1]) 45 - .map_err(|e| format!("Base64 decode failed: {}", e))?; 46 - 47 - let claims: UnsafeClaims = serde_json::from_slice(&payload_bytes) 48 - .map_err(|e| format!("JSON decode failed: {}", e))?; 49 - 50 - Ok(claims.sub.unwrap_or(claims.iss)) 51 - } 52 - 53 - pub fn create_access_token(did: &str, key_bytes: &[u8]) -> Result<String, anyhow::Error> { 54 - create_signed_token(did, "access", key_bytes, Duration::minutes(15)) 55 - } 56 - 57 - pub fn create_refresh_token(did: &str, key_bytes: &[u8]) -> Result<String, anyhow::Error> { 58 - create_signed_token(did, "refresh", key_bytes, Duration::days(7)) 59 - } 60 - 61 - pub fn create_service_token(did: &str, aud: &str, lxm: &str, key_bytes: &[u8]) -> Result<String, anyhow::Error> { 62 - let signing_key = SigningKey::from_slice(key_bytes)?; 63 - 64 - let expiration = Utc::now() 65 - .checked_add_signed(Duration::seconds(60)) 66 - .expect("valid timestamp") 67 - .timestamp(); 68 - 69 - let claims = Claims { 70 - iss: did.to_owned(), 71 - sub: did.to_owned(), 72 - aud: aud.to_owned(), 73 - exp: expiration as usize, 74 - iat: Utc::now().timestamp() as usize, 75 - scope: None, 76 - lxm: Some(lxm.to_string()), 77 - jti: uuid::Uuid::new_v4().to_string(), 78 - }; 79 - 80 - sign_claims(claims, &signing_key) 81 - } 82 - 83 - fn create_signed_token(did: &str, scope: &str, key_bytes: &[u8], duration: Duration) -> Result<String, anyhow::Error> { 84 - let signing_key = SigningKey::from_slice(key_bytes)?; 85 - 86 - let expiration = Utc::now() 87 - .checked_add_signed(duration) 88 - .expect("valid timestamp") 89 - .timestamp(); 90 - 91 - let claims = Claims { 92 - iss: did.to_owned(), 93 - sub: did.to_owned(), 94 - aud: format!("did:web:{}", std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string())), 95 - exp: expiration as usize, 96 - iat: Utc::now().timestamp() as usize, 97 - scope: Some(scope.to_string()), 98 - lxm: None, 99 - jti: uuid::Uuid::new_v4().to_string(), 100 - }; 101 - 102 - sign_claims(claims, &signing_key) 103 - } 104 - 105 - fn sign_claims(claims: Claims, key: &SigningKey) -> Result<String, anyhow::Error> { 106 - let header = Header { 107 - alg: "ES256K".to_string(), 108 - typ: "JWT".to_string(), 109 - }; 110 - 111 - let header_json = serde_json::to_string(&header)?; 112 - let claims_json = serde_json::to_string(&claims)?; 113 - 114 - let header_b64 = URL_SAFE_NO_PAD.encode(header_json); 115 - let claims_b64 = URL_SAFE_NO_PAD.encode(claims_json); 116 - 117 - let message = format!("{}.{}", header_b64, claims_b64); 118 - let signature: Signature = key.sign(message.as_bytes()); 119 - let signature_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes()); 120 - 121 - Ok(format!("{}.{}", message, signature_b64)) 122 - } 123 - 124 - pub fn verify_token(token: &str, key_bytes: &[u8]) -> Result<TokenData<Claims>, anyhow::Error> { 125 - let parts: Vec<&str> = token.split('.').collect(); 126 - if parts.len() != 3 { 127 - return Err(anyhow!("Invalid token format")); 128 - } 129 - 130 - let header_b64 = parts[0]; 131 - let claims_b64 = parts[1]; 132 - let signature_b64 = parts[2]; 133 - 134 - let signature_bytes = URL_SAFE_NO_PAD.decode(signature_b64) 135 - .context("Base64 decode of signature failed")?; 136 - let signature = Signature::from_slice(&signature_bytes) 137 - .map_err(|e| anyhow!("Invalid signature format: {}", e))?; 138 - 139 - let signing_key = SigningKey::from_slice(key_bytes)?; 140 - let verifying_key = VerifyingKey::from(&signing_key); 141 - 142 - let message = format!("{}.{}", header_b64, claims_b64); 143 - verifying_key.verify(message.as_bytes(), &signature) 144 - .map_err(|e| anyhow!("Signature verification failed: {}", e))?; 145 - 146 - let claims_bytes = URL_SAFE_NO_PAD.decode(claims_b64) 147 - .context("Base64 decode of claims failed")?; 148 - let claims: Claims = serde_json::from_slice(&claims_bytes) 149 - .context("JSON decode of claims failed")?; 150 - 151 - let now = Utc::now().timestamp() as usize; 152 - if claims.exp < now { 153 - return Err(anyhow!("Token expired")); 154 - } 155 - 156 - Ok(TokenData { claims }) 157 - }
···
+38
src/auth/mod.rs
···
··· 1 + use serde::{Deserialize, Serialize}; 2 + 3 + pub mod token; 4 + pub mod verify; 5 + 6 + pub use token::{create_access_token, create_refresh_token, create_service_token}; 7 + pub use verify::{get_did_from_token, verify_token}; 8 + 9 + #[derive(Debug, Serialize, Deserialize)] 10 + pub struct Claims { 11 + pub iss: String, 12 + pub sub: String, 13 + pub aud: String, 14 + pub exp: usize, 15 + pub iat: usize, 16 + #[serde(skip_serializing_if = "Option::is_none")] 17 + pub scope: Option<String>, 18 + #[serde(skip_serializing_if = "Option::is_none")] 19 + pub lxm: Option<String>, 20 + pub jti: String, 21 + } 22 + 23 + #[derive(Debug, Serialize, Deserialize)] 24 + pub struct Header { 25 + pub alg: String, 26 + pub typ: String, 27 + } 28 + 29 + #[derive(Debug, Serialize, Deserialize)] 30 + pub struct UnsafeClaims { 31 + pub iss: String, 32 + pub sub: Option<String>, 33 + } 34 + 35 + // fancy boy TokenData equivalent for compatibility/structure 36 + pub struct TokenData<T> { 37 + pub claims: T, 38 + }
+86
src/auth/token.rs
···
··· 1 + use super::{Claims, Header}; 2 + use anyhow::Result; 3 + use base64::Engine as _; 4 + use base64::engine::general_purpose::URL_SAFE_NO_PAD; 5 + use chrono::{Duration, Utc}; 6 + use k256::ecdsa::{Signature, SigningKey, signature::Signer}; 7 + use uuid; 8 + 9 + pub fn create_access_token(did: &str, key_bytes: &[u8]) -> Result<String> { 10 + create_signed_token(did, "access", key_bytes, Duration::minutes(15)) 11 + } 12 + 13 + pub fn create_refresh_token(did: &str, key_bytes: &[u8]) -> Result<String> { 14 + create_signed_token(did, "refresh", key_bytes, Duration::days(7)) 15 + } 16 + 17 + pub fn create_service_token(did: &str, aud: &str, lxm: &str, key_bytes: &[u8]) -> Result<String> { 18 + let signing_key = SigningKey::from_slice(key_bytes)?; 19 + 20 + let expiration = Utc::now() 21 + .checked_add_signed(Duration::seconds(60)) 22 + .expect("valid timestamp") 23 + .timestamp(); 24 + 25 + let claims = Claims { 26 + iss: did.to_owned(), 27 + sub: did.to_owned(), 28 + aud: aud.to_owned(), 29 + exp: expiration as usize, 30 + iat: Utc::now().timestamp() as usize, 31 + scope: None, 32 + lxm: Some(lxm.to_string()), 33 + jti: uuid::Uuid::new_v4().to_string(), 34 + }; 35 + 36 + sign_claims(claims, &signing_key) 37 + } 38 + 39 + fn create_signed_token( 40 + did: &str, 41 + scope: &str, 42 + key_bytes: &[u8], 43 + duration: Duration, 44 + ) -> Result<String> { 45 + let signing_key = SigningKey::from_slice(key_bytes)?; 46 + 47 + let expiration = Utc::now() 48 + .checked_add_signed(duration) 49 + .expect("valid timestamp") 50 + .timestamp(); 51 + 52 + let claims = Claims { 53 + iss: did.to_owned(), 54 + sub: did.to_owned(), 55 + aud: format!( 56 + "did:web:{}", 57 + std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()) 58 + ), 59 + exp: expiration as usize, 60 + iat: Utc::now().timestamp() as usize, 61 + scope: Some(scope.to_string()), 62 + lxm: None, 63 + jti: uuid::Uuid::new_v4().to_string(), 64 + }; 65 + 66 + sign_claims(claims, &signing_key) 67 + } 68 + 69 + fn sign_claims(claims: Claims, key: &SigningKey) -> Result<String> { 70 + let header = Header { 71 + alg: "ES256K".to_string(), 72 + typ: "JWT".to_string(), 73 + }; 74 + 75 + let header_json = serde_json::to_string(&header)?; 76 + let claims_json = serde_json::to_string(&claims)?; 77 + 78 + let header_b64 = URL_SAFE_NO_PAD.encode(header_json); 79 + let claims_b64 = URL_SAFE_NO_PAD.encode(claims_json); 80 + 81 + let message = format!("{}.{}", header_b64, claims_b64); 82 + let signature: Signature = key.sign(message.as_bytes()); 83 + let signature_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes()); 84 + 85 + Ok(format!("{}.{}", message, signature_b64)) 86 + }
+60
src/auth/verify.rs
···
··· 1 + use super::{Claims, TokenData, UnsafeClaims}; 2 + use anyhow::{Context, Result, anyhow}; 3 + use base64::Engine as _; 4 + use base64::engine::general_purpose::URL_SAFE_NO_PAD; 5 + use chrono::Utc; 6 + use k256::ecdsa::{Signature, SigningKey, VerifyingKey, signature::Verifier}; 7 + 8 + pub fn get_did_from_token(token: &str) -> Result<String, String> { 9 + let parts: Vec<&str> = token.split('.').collect(); 10 + if parts.len() != 3 { 11 + return Err("Invalid token format".to_string()); 12 + } 13 + 14 + let payload_bytes = URL_SAFE_NO_PAD 15 + .decode(parts[1]) 16 + .map_err(|e| format!("Base64 decode failed: {}", e))?; 17 + 18 + let claims: UnsafeClaims = 19 + serde_json::from_slice(&payload_bytes).map_err(|e| format!("JSON decode failed: {}", e))?; 20 + 21 + Ok(claims.sub.unwrap_or(claims.iss)) 22 + } 23 + 24 + pub fn verify_token(token: &str, key_bytes: &[u8]) -> Result<TokenData<Claims>> { 25 + let parts: Vec<&str> = token.split('.').collect(); 26 + if parts.len() != 3 { 27 + return Err(anyhow!("Invalid token format")); 28 + } 29 + 30 + let header_b64 = parts[0]; 31 + let claims_b64 = parts[1]; 32 + let signature_b64 = parts[2]; 33 + 34 + let signature_bytes = URL_SAFE_NO_PAD 35 + .decode(signature_b64) 36 + .context("Base64 decode of signature failed")?; 37 + let signature = Signature::from_slice(&signature_bytes) 38 + .map_err(|e| anyhow!("Invalid signature format: {}", e))?; 39 + 40 + let signing_key = SigningKey::from_slice(key_bytes)?; 41 + let verifying_key = VerifyingKey::from(&signing_key); 42 + 43 + let message = format!("{}.{}", header_b64, claims_b64); 44 + verifying_key 45 + .verify(message.as_bytes(), &signature) 46 + .map_err(|e| anyhow!("Signature verification failed: {}", e))?; 47 + 48 + let claims_bytes = URL_SAFE_NO_PAD 49 + .decode(claims_b64) 50 + .context("Base64 decode of claims failed")?; 51 + let claims: Claims = 52 + serde_json::from_slice(&claims_bytes).context("JSON decode of claims failed")?; 53 + 54 + let now = Utc::now().timestamp() as usize; 55 + if claims.exp < now { 56 + return Err(anyhow!("Token expired")); 57 + } 58 + 59 + Ok(TokenData { claims }) 60 + }
+54 -15
src/lib.rs
··· 1 pub mod api; 2 - pub mod state; 3 pub mod auth; 4 pub mod repo; 5 pub mod storage; 6 7 use axum::{ 8 - routing::{get, post, any}, 9 Router, 10 }; 11 use state::AppState; 12 13 pub fn app(state: AppState) -> Router { 14 Router::new() 15 .route("/health", get(api::server::health)) 16 - .route("/xrpc/com.atproto.server.describeServer", get(api::server::describe_server)) 17 - .route("/xrpc/com.atproto.server.createAccount", post(api::identity::create_account)) 18 - .route("/xrpc/com.atproto.server.createSession", post(api::server::create_session)) 19 - .route("/xrpc/com.atproto.server.getSession", get(api::server::get_session)) 20 - .route("/xrpc/com.atproto.server.deleteSession", post(api::server::delete_session)) 21 - .route("/xrpc/com.atproto.server.refreshSession", post(api::server::refresh_session)) 22 - .route("/xrpc/com.atproto.repo.createRecord", post(api::repo::create_record)) 23 - .route("/xrpc/com.atproto.repo.putRecord", post(api::repo::put_record)) 24 - .route("/xrpc/com.atproto.repo.getRecord", get(api::repo::get_record)) 25 - .route("/xrpc/com.atproto.repo.deleteRecord", post(api::repo::delete_record)) 26 - .route("/xrpc/com.atproto.repo.listRecords", get(api::repo::list_records)) 27 - .route("/xrpc/com.atproto.repo.describeRepo", get(api::repo::describe_repo)) 28 - .route("/xrpc/com.atproto.repo.uploadBlob", post(api::repo::upload_blob)) 29 .route("/.well-known/did.json", get(api::identity::well_known_did)) 30 .route("/u/{handle}/did.json", get(api::identity::user_did_doc)) 31 .route("/xrpc/{*method}", any(api::proxy::proxy_handler))
··· 1 pub mod api; 2 pub mod auth; 3 pub mod repo; 4 + pub mod state; 5 pub mod storage; 6 7 use axum::{ 8 Router, 9 + routing::{any, get, post}, 10 }; 11 use state::AppState; 12 13 pub fn app(state: AppState) -> Router { 14 Router::new() 15 .route("/health", get(api::server::health)) 16 + .route( 17 + "/xrpc/com.atproto.server.describeServer", 18 + get(api::server::describe_server), 19 + ) 20 + .route( 21 + "/xrpc/com.atproto.server.createAccount", 22 + post(api::identity::create_account), 23 + ) 24 + .route( 25 + "/xrpc/com.atproto.server.createSession", 26 + post(api::server::create_session), 27 + ) 28 + .route( 29 + "/xrpc/com.atproto.server.getSession", 30 + get(api::server::get_session), 31 + ) 32 + .route( 33 + "/xrpc/com.atproto.server.deleteSession", 34 + post(api::server::delete_session), 35 + ) 36 + .route( 37 + "/xrpc/com.atproto.server.refreshSession", 38 + post(api::server::refresh_session), 39 + ) 40 + .route( 41 + "/xrpc/com.atproto.repo.createRecord", 42 + post(api::repo::create_record), 43 + ) 44 + .route( 45 + "/xrpc/com.atproto.repo.putRecord", 46 + post(api::repo::put_record), 47 + ) 48 + .route( 49 + "/xrpc/com.atproto.repo.getRecord", 50 + get(api::repo::get_record), 51 + ) 52 + .route( 53 + "/xrpc/com.atproto.repo.deleteRecord", 54 + post(api::repo::delete_record), 55 + ) 56 + .route( 57 + "/xrpc/com.atproto.repo.listRecords", 58 + get(api::repo::list_records), 59 + ) 60 + .route( 61 + "/xrpc/com.atproto.repo.describeRepo", 62 + get(api::repo::describe_repo), 63 + ) 64 + .route( 65 + "/xrpc/com.atproto.repo.uploadBlob", 66 + post(api::repo::upload_blob), 67 + ) 68 .route("/.well-known/did.json", get(api::identity::well_known_did)) 69 .route("/u/{handle}/did.json", get(api::identity::user_did_doc)) 70 .route("/xrpc/{*method}", any(api::proxy::proxy_handler))
+1 -1
src/main.rs
··· 1 - use std::net::SocketAddr; 2 use bspds::state::AppState; 3 use tracing::info; 4 5 #[tokio::main]
··· 1 use bspds::state::AppState; 2 + use std::net::SocketAddr; 3 use tracing::info; 4 5 #[tokio::main]
+18 -13
src/repo/mod.rs
··· 1 - use jacquard_repo::storage::BlockStore; 2 use jacquard_repo::error::RepoError; 3 use jacquard_repo::repo::CommitData; 4 - use cid::Cid; 5 - use sqlx::{PgPool, Row}; 6 - use bytes::Bytes; 7 - use sha2::{Sha256, Digest}; 8 use multihash::Multihash; 9 10 #[derive(Clone)] 11 pub struct PostgresBlockStore { ··· 31 Some(row) => { 32 let data: Vec<u8> = row.get("data"); 33 Ok(Some(Bytes::from(data))) 34 - }, 35 None => Ok(None), 36 } 37 } ··· 65 Ok(row.is_some()) 66 } 67 68 - async fn put_many(&self, blocks: impl IntoIterator<Item = (Cid, Bytes)> + Send) -> Result<(), RepoError> { 69 let blocks: Vec<_> = blocks.into_iter().collect(); 70 for (cid, data) in blocks { 71 let cid_bytes = cid.to_bytes(); 72 - sqlx::query("INSERT INTO blocks (cid, data) VALUES ($1, $2) ON CONFLICT (cid) DO NOTHING") 73 - .bind(cid_bytes) 74 - .bind(data.as_ref()) 75 - .execute(&self.pool) 76 - .await 77 - .map_err(|e| RepoError::storage(e))?; 78 } 79 Ok(()) 80 }
··· 1 + use bytes::Bytes; 2 + use cid::Cid; 3 use jacquard_repo::error::RepoError; 4 use jacquard_repo::repo::CommitData; 5 + use jacquard_repo::storage::BlockStore; 6 use multihash::Multihash; 7 + use sha2::{Digest, Sha256}; 8 + use sqlx::{PgPool, Row}; 9 10 #[derive(Clone)] 11 pub struct PostgresBlockStore { ··· 31 Some(row) => { 32 let data: Vec<u8> = row.get("data"); 33 Ok(Some(Bytes::from(data))) 34 + } 35 None => Ok(None), 36 } 37 } ··· 65 Ok(row.is_some()) 66 } 67 68 + async fn put_many( 69 + &self, 70 + blocks: impl IntoIterator<Item = (Cid, Bytes)> + Send, 71 + ) -> Result<(), RepoError> { 72 let blocks: Vec<_> = blocks.into_iter().collect(); 73 for (cid, data) in blocks { 74 let cid_bytes = cid.to_bytes(); 75 + sqlx::query( 76 + "INSERT INTO blocks (cid, data) VALUES ($1, $2) ON CONFLICT (cid) DO NOTHING", 77 + ) 78 + .bind(cid_bytes) 79 + .bind(data.as_ref()) 80 + .execute(&self.pool) 81 + .await 82 + .map_err(|e| RepoError::storage(e))?; 83 } 84 Ok(()) 85 }
+6 -2
src/state.rs
··· 1 - use sqlx::PgPool; 2 use crate::repo::PostgresBlockStore; 3 use crate::storage::{BlobStorage, S3BlobStorage}; 4 use std::sync::Arc; 5 6 #[derive(Clone)] ··· 14 pub async fn new(db: PgPool) -> Self { 15 let block_store = PostgresBlockStore::new(db.clone()); 16 let blob_store = S3BlobStorage::new().await; 17 - Self { db, block_store, blob_store: Arc::new(blob_store) } 18 } 19 }
··· 1 use crate::repo::PostgresBlockStore; 2 use crate::storage::{BlobStorage, S3BlobStorage}; 3 + use sqlx::PgPool; 4 use std::sync::Arc; 5 6 #[derive(Clone)] ··· 14 pub async fn new(db: PgPool) -> Self { 15 let block_store = PostgresBlockStore::new(db.clone()); 16 let blob_store = S3BlobStorage::new().await; 17 + Self { 18 + db, 19 + block_store, 20 + blob_store: Arc::new(blob_store), 21 + } 22 } 23 }
+14 -7
src/storage/mod.rs
··· 1 use async_trait::async_trait; 2 - use thiserror::Error; 3 use aws_sdk_s3::Client; 4 use aws_sdk_s3::primitives::ByteStream; 5 - use aws_config::meta::region::RegionProviderChain; 6 - use aws_config::BehaviorVersion; 7 8 #[derive(Error, Debug)] 9 pub enum StorageError { ··· 55 #[async_trait] 56 impl BlobStorage for S3BlobStorage { 57 async fn put(&self, key: &str, data: &[u8]) -> Result<(), StorageError> { 58 - self.client.put_object() 59 .bucket(&self.bucket) 60 .key(key) 61 .body(ByteStream::from(data.to_vec())) ··· 66 } 67 68 async fn get(&self, key: &str) -> Result<Vec<u8>, StorageError> { 69 - let resp = self.client.get_object() 70 .bucket(&self.bucket) 71 .key(key) 72 .send() 73 .await 74 .map_err(|e| StorageError::S3(e.to_string()))?; 75 76 - let data = resp.body.collect().await 77 .map_err(|e| StorageError::S3(e.to_string()))? 78 .into_bytes(); 79 ··· 81 } 82 83 async fn delete(&self, key: &str) -> Result<(), StorageError> { 84 - self.client.delete_object() 85 .bucket(&self.bucket) 86 .key(key) 87 .send()
··· 1 use async_trait::async_trait; 2 + use aws_config::BehaviorVersion; 3 + use aws_config::meta::region::RegionProviderChain; 4 use aws_sdk_s3::Client; 5 use aws_sdk_s3::primitives::ByteStream; 6 + use thiserror::Error; 7 8 #[derive(Error, Debug)] 9 pub enum StorageError { ··· 55 #[async_trait] 56 impl BlobStorage for S3BlobStorage { 57 async fn put(&self, key: &str, data: &[u8]) -> Result<(), StorageError> { 58 + self.client 59 + .put_object() 60 .bucket(&self.bucket) 61 .key(key) 62 .body(ByteStream::from(data.to_vec())) ··· 67 } 68 69 async fn get(&self, key: &str) -> Result<Vec<u8>, StorageError> { 70 + let resp = self 71 + .client 72 + .get_object() 73 .bucket(&self.bucket) 74 .key(key) 75 .send() 76 .await 77 .map_err(|e| StorageError::S3(e.to_string()))?; 78 79 + let data = resp 80 + .body 81 + .collect() 82 + .await 83 .map_err(|e| StorageError::S3(e.to_string()))? 84 .into_bytes(); 85 ··· 87 } 88 89 async fn delete(&self, key: &str) -> Result<(), StorageError> { 90 + self.client 91 + .delete_object() 92 .bucket(&self.bucket) 93 .key(key) 94 .send()
+5 -4
tests/auth.rs
··· 1 use bspds::auth; 2 use k256::SecretKey; 3 use rand::rngs::OsRng; 4 - use chrono::{Utc, Duration}; 5 - use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 6 use serde_json::json; 7 - use k256::ecdsa::{SigningKey, signature::Signer}; 8 9 #[test] 10 fn test_jwt_flow() { ··· 24 25 let aud = "did:web:service"; 26 let lxm = "com.example.test"; 27 - let s_token = auth::create_service_token(did, aud, lxm, &key_bytes).expect("create service token"); 28 let s_data = auth::verify_token(&s_token, &key_bytes).expect("verify service token"); 29 assert_eq!(s_data.claims.aud, aud); 30 assert_eq!(s_data.claims.lxm, Some(lxm.to_string()));
··· 1 + use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 2 use bspds::auth; 3 + use chrono::{Duration, Utc}; 4 use k256::SecretKey; 5 + use k256::ecdsa::{SigningKey, signature::Signer}; 6 use rand::rngs::OsRng; 7 use serde_json::json; 8 9 #[test] 10 fn test_jwt_flow() { ··· 24 25 let aud = "did:web:service"; 26 let lxm = "com.example.test"; 27 + let s_token = 28 + auth::create_service_token(did, aud, lxm, &key_bytes).expect("create service token"); 29 let s_data = auth::verify_token(&s_token, &key_bytes).expect("verify service token"); 30 assert_eq!(s_data.claims.aud, aud); 31 assert_eq!(s_data.claims.lxm, Some(lxm.to_string()));
+77 -27
tests/common/mod.rs
··· 1 - use reqwest::{header, Client, StatusCode}; 2 - use serde_json::{json, Value}; 3 use chrono::Utc; 4 #[allow(unused_imports)] 5 use std::collections::HashMap; 6 #[allow(unused_imports)] 7 use std::time::Duration; 8 - use std::sync::OnceLock; 9 - use bspds::state::AppState; 10 - use sqlx::postgres::PgPoolOptions; 11 - use tokio::net::TcpListener; 12 - use testcontainers::{runners::AsyncRunner, ContainerAsync, ImageExt, GenericImage}; 13 use testcontainers::core::ContainerPort; 14 use testcontainers_modules::postgres::Postgres; 15 - use aws_sdk_s3::Client as S3Client; 16 - use aws_config::BehaviorVersion; 17 - use aws_sdk_s3::config::Credentials; 18 - use wiremock::{MockServer, Mock, ResponseTemplate}; 19 use wiremock::matchers::{method, path}; 20 21 static SERVER_URL: OnceLock<String> = OnceLock::new(); 22 static DB_CONTAINER: OnceLock<ContainerAsync<Postgres>> = OnceLock::new(); ··· 46 if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") { 47 let podman_sock = std::path::Path::new(&runtime_dir).join("podman/podman.sock"); 48 if podman_sock.exists() { 49 - unsafe { std::env::set_var("DOCKER_HOST", format!("unix://{}", podman_sock.display())); } 50 } 51 } 52 } ··· 62 .await 63 .expect("Failed to start MinIO"); 64 65 - let s3_port = s3_container.get_host_port_ipv4(9000).await.expect("Failed to get S3 port"); 66 let s3_endpoint = format!("http://127.0.0.1:{}", s3_port); 67 68 unsafe { ··· 76 let sdk_config = aws_config::defaults(BehaviorVersion::latest()) 77 .region("us-east-1") 78 .endpoint_url(&s3_endpoint) 79 - .credentials_provider(Credentials::new("minioadmin", "minioadmin", None, None, "test")) 80 .load() 81 .await; 82 ··· 108 .mount(&mock_server) 109 .await; 110 111 - unsafe { std::env::set_var("APPVIEW_URL", mock_server.uri()); } 112 MOCK_APPVIEW.set(mock_server).ok(); 113 114 S3_CONTAINER.set(s3_container).ok(); 115 116 - let container = Postgres::default().with_tag("18-alpine").start().await.expect("Failed to start Postgres"); 117 let connection_string = format!( 118 "postgres://postgres:postgres@127.0.0.1:{}/postgres", 119 - container.get_host_port_ipv4(5432).await.expect("Failed to get port") 120 ); 121 122 DB_CONTAINER.set(container).ok(); ··· 157 158 #[allow(dead_code)] 159 pub async fn upload_test_blob(client: &Client, data: &'static str, mime: &'static str) -> Value { 160 - let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await)) 161 .header(header::CONTENT_TYPE, mime) 162 .bearer_auth(AUTH_TOKEN) 163 .body(data) ··· 170 body["blob"].clone() 171 } 172 173 - 174 #[allow(dead_code)] 175 pub async fn create_test_post( 176 client: &Client, 177 text: &str, 178 - reply_to: Option<Value> 179 ) -> (String, String, String) { 180 let collection = "app.bsky.feed.post"; 181 let mut record = json!({ ··· 194 "record": record 195 }); 196 197 - let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await)) 198 .bearer_auth(AUTH_TOKEN) 199 .json(&payload) 200 .send() ··· 202 .expect("Failed to send createRecord"); 203 204 assert_eq!(res.status(), StatusCode::OK, "Failed to create post record"); 205 - let body: Value = res.json().await.expect("createRecord response was not JSON"); 206 207 - let uri = body["uri"].as_str().expect("Response had no URI").to_string(); 208 - let cid = body["cid"].as_str().expect("Response had no CID").to_string(); 209 - let rkey = uri.split('/').last().expect("URI was malformed").to_string(); 210 211 (uri, cid, rkey) 212 } ··· 220 "password": "password" 221 }); 222 223 - let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await)) 224 .json(&payload) 225 .send() 226 .await ··· 231 } 232 233 let body: Value = res.json().await.expect("Invalid JSON"); 234 - let access_jwt = body["accessJwt"].as_str().expect("No accessJwt").to_string(); 235 let did = body["did"].as_str().expect("No did").to_string(); 236 (access_jwt, did) 237 }
··· 1 + use aws_config::BehaviorVersion; 2 + use aws_sdk_s3::Client as S3Client; 3 + use aws_sdk_s3::config::Credentials; 4 + use bspds::state::AppState; 5 use chrono::Utc; 6 + use reqwest::{Client, StatusCode, header}; 7 + use serde_json::{Value, json}; 8 + use sqlx::postgres::PgPoolOptions; 9 #[allow(unused_imports)] 10 use std::collections::HashMap; 11 + use std::sync::OnceLock; 12 #[allow(unused_imports)] 13 use std::time::Duration; 14 use testcontainers::core::ContainerPort; 15 + use testcontainers::{ContainerAsync, GenericImage, ImageExt, runners::AsyncRunner}; 16 use testcontainers_modules::postgres::Postgres; 17 + use tokio::net::TcpListener; 18 use wiremock::matchers::{method, path}; 19 + use wiremock::{Mock, MockServer, ResponseTemplate}; 20 21 static SERVER_URL: OnceLock<String> = OnceLock::new(); 22 static DB_CONTAINER: OnceLock<ContainerAsync<Postgres>> = OnceLock::new(); ··· 46 if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") { 47 let podman_sock = std::path::Path::new(&runtime_dir).join("podman/podman.sock"); 48 if podman_sock.exists() { 49 + unsafe { 50 + std::env::set_var( 51 + "DOCKER_HOST", 52 + format!("unix://{}", podman_sock.display()), 53 + ); 54 + } 55 } 56 } 57 } ··· 67 .await 68 .expect("Failed to start MinIO"); 69 70 + let s3_port = s3_container 71 + .get_host_port_ipv4(9000) 72 + .await 73 + .expect("Failed to get S3 port"); 74 let s3_endpoint = format!("http://127.0.0.1:{}", s3_port); 75 76 unsafe { ··· 84 let sdk_config = aws_config::defaults(BehaviorVersion::latest()) 85 .region("us-east-1") 86 .endpoint_url(&s3_endpoint) 87 + .credentials_provider(Credentials::new( 88 + "minioadmin", 89 + "minioadmin", 90 + None, 91 + None, 92 + "test", 93 + )) 94 .load() 95 .await; 96 ··· 122 .mount(&mock_server) 123 .await; 124 125 + unsafe { 126 + std::env::set_var("APPVIEW_URL", mock_server.uri()); 127 + } 128 MOCK_APPVIEW.set(mock_server).ok(); 129 130 S3_CONTAINER.set(s3_container).ok(); 131 132 + let container = Postgres::default() 133 + .with_tag("18-alpine") 134 + .start() 135 + .await 136 + .expect("Failed to start Postgres"); 137 let connection_string = format!( 138 "postgres://postgres:postgres@127.0.0.1:{}/postgres", 139 + container 140 + .get_host_port_ipv4(5432) 141 + .await 142 + .expect("Failed to get port") 143 ); 144 145 DB_CONTAINER.set(container).ok(); ··· 180 181 #[allow(dead_code)] 182 pub async fn upload_test_blob(client: &Client, data: &'static str, mime: &'static str) -> Value { 183 + let res = client 184 + .post(format!( 185 + "{}/xrpc/com.atproto.repo.uploadBlob", 186 + base_url().await 187 + )) 188 .header(header::CONTENT_TYPE, mime) 189 .bearer_auth(AUTH_TOKEN) 190 .body(data) ··· 197 body["blob"].clone() 198 } 199 200 #[allow(dead_code)] 201 pub async fn create_test_post( 202 client: &Client, 203 text: &str, 204 + reply_to: Option<Value>, 205 ) -> (String, String, String) { 206 let collection = "app.bsky.feed.post"; 207 let mut record = json!({ ··· 220 "record": record 221 }); 222 223 + let res = client 224 + .post(format!( 225 + "{}/xrpc/com.atproto.repo.createRecord", 226 + base_url().await 227 + )) 228 .bearer_auth(AUTH_TOKEN) 229 .json(&payload) 230 .send() ··· 232 .expect("Failed to send createRecord"); 233 234 assert_eq!(res.status(), StatusCode::OK, "Failed to create post record"); 235 + let body: Value = res 236 + .json() 237 + .await 238 + .expect("createRecord response was not JSON"); 239 240 + let uri = body["uri"] 241 + .as_str() 242 + .expect("Response had no URI") 243 + .to_string(); 244 + let cid = body["cid"] 245 + .as_str() 246 + .expect("Response had no CID") 247 + .to_string(); 248 + let rkey = uri 249 + .split('/') 250 + .last() 251 + .expect("URI was malformed") 252 + .to_string(); 253 254 (uri, cid, rkey) 255 } ··· 263 "password": "password" 264 }); 265 266 + let res = client 267 + .post(format!( 268 + "{}/xrpc/com.atproto.server.createAccount", 269 + base_url().await 270 + )) 271 .json(&payload) 272 .send() 273 .await ··· 278 } 279 280 let body: Value = res.json().await.expect("Invalid JSON"); 281 + let access_jwt = body["accessJwt"] 282 + .as_str() 283 + .expect("No accessJwt") 284 + .to_string(); 285 let did = body["did"].as_str().expect("No did").to_string(); 286 (access_jwt, did) 287 }
+39 -11
tests/identity.rs
··· 1 mod common; 2 use common::*; 3 use reqwest::StatusCode; 4 - use serde_json::{json, Value}; 5 - use wiremock::{MockServer, Mock, ResponseTemplate}; 6 use wiremock::matchers::{method, path}; 7 8 // #[tokio::test] 9 // async fn test_resolve_handle() { ··· 23 #[tokio::test] 24 async fn test_well_known_did() { 25 let client = client(); 26 - let res = client.get(format!("{}/.well-known/did.json", base_url().await)) 27 .send() 28 .await 29 .expect("Failed to send request"); ··· 71 "did": did 72 }); 73 74 - let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await)) 75 .json(&payload) 76 .send() 77 .await ··· 79 80 if res.status() != StatusCode::OK { 81 let status = res.status(); 82 - let body: Value = res.json().await.unwrap_or(json!({"error": "could not parse body"})); 83 panic!("createAccount failed with status {}: {:?}", status, body); 84 } 85 - let body: Value = res.json().await.expect("createAccount response was not JSON"); 86 assert_eq!(body["did"], did); 87 88 - let res = client.get(format!("{}/u/{}/did.json", base_url().await, handle)) 89 .send() 90 .await 91 .expect("Failed to fetch DID doc"); ··· 111 "password": "password" 112 }); 113 114 - let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await)) 115 .json(&payload) 116 .send() 117 .await 118 .expect("Failed to send request"); 119 assert_eq!(res.status(), StatusCode::OK); 120 121 - let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await)) 122 .json(&payload) 123 .send() 124 .await ··· 143 "did": did 144 }); 145 146 - let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await)) 147 .json(&create_payload) 148 .send() 149 .await ··· 162 "identifier": handle, 163 "password": "password" 164 }); 165 - let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", base_url().await)) 166 .json(&login_payload) 167 .send() 168 .await
··· 1 mod common; 2 use common::*; 3 use reqwest::StatusCode; 4 + use serde_json::{Value, json}; 5 use wiremock::matchers::{method, path}; 6 + use wiremock::{Mock, MockServer, ResponseTemplate}; 7 8 // #[tokio::test] 9 // async fn test_resolve_handle() { ··· 23 #[tokio::test] 24 async fn test_well_known_did() { 25 let client = client(); 26 + let res = client 27 + .get(format!("{}/.well-known/did.json", base_url().await)) 28 .send() 29 .await 30 .expect("Failed to send request"); ··· 72 "did": did 73 }); 74 75 + let res = client 76 + .post(format!( 77 + "{}/xrpc/com.atproto.server.createAccount", 78 + base_url().await 79 + )) 80 .json(&payload) 81 .send() 82 .await ··· 84 85 if res.status() != StatusCode::OK { 86 let status = res.status(); 87 + let body: Value = res 88 + .json() 89 + .await 90 + .unwrap_or(json!({"error": "could not parse body"})); 91 panic!("createAccount failed with status {}: {:?}", status, body); 92 } 93 + let body: Value = res 94 + .json() 95 + .await 96 + .expect("createAccount response was not JSON"); 97 assert_eq!(body["did"], did); 98 99 + let res = client 100 + .get(format!("{}/u/{}/did.json", base_url().await, handle)) 101 .send() 102 .await 103 .expect("Failed to fetch DID doc"); ··· 123 "password": "password" 124 }); 125 126 + let res = client 127 + .post(format!( 128 + "{}/xrpc/com.atproto.server.createAccount", 129 + base_url().await 130 + )) 131 .json(&payload) 132 .send() 133 .await 134 .expect("Failed to send request"); 135 assert_eq!(res.status(), StatusCode::OK); 136 137 + let res = client 138 + .post(format!( 139 + "{}/xrpc/com.atproto.server.createAccount", 140 + base_url().await 141 + )) 142 .json(&payload) 143 .send() 144 .await ··· 163 "did": did 164 }); 165 166 + let res = client 167 + .post(format!( 168 + "{}/xrpc/com.atproto.server.createAccount", 169 + base_url().await 170 + )) 171 .json(&create_payload) 172 .send() 173 .await ··· 186 "identifier": handle, 187 "password": "password" 188 }); 189 + let res = client 190 + .post(format!( 191 + "{}/xrpc/com.atproto.server.createSession", 192 + base_url().await 193 + )) 194 .json(&login_payload) 195 .send() 196 .await
+376 -45
tests/lifecycle.rs
··· 1 mod common; 2 use common::*; 3 4 - use reqwest::{Client, StatusCode}; 5 - use serde_json::{json, Value}; 6 use chrono::Utc; 7 - #[allow(unused_imports)] 8 use std::time::Duration; 9 10 async fn setup_new_user(handle_prefix: &str) -> (String, String) { ··· 19 "email": email, 20 "password": password 21 }); 22 - let create_res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await)) 23 .json(&create_account_payload) 24 .send() 25 .await 26 .expect("setup_new_user: Failed to send createAccount"); 27 28 - if create_res.status() != StatusCode::OK { 29 - panic!("setup_new_user: Failed to create account: {:?}", create_res.text().await); 30 } 31 32 - let create_body: Value = create_res.json().await.expect("setup_new_user: createAccount response was not JSON"); 33 34 - let new_did = create_body["did"].as_str().expect("setup_new_user: Response had no DID").to_string(); 35 - let new_jwt = create_body["accessJwt"].as_str().expect("setup_new_user: Response had no accessJwt").to_string(); 36 37 (new_did, new_jwt) 38 } ··· 59 } 60 }); 61 62 - let create_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 63 .bearer_auth(&jwt) 64 .json(&create_payload) 65 .send() 66 .await 67 .expect("Failed to send create request"); 68 69 - assert_eq!(create_res.status(), StatusCode::OK, "Failed to create record"); 70 - let create_body: Value = create_res.json().await.expect("create response was not JSON"); 71 - let uri = create_body["uri"].as_str().unwrap(); 72 73 74 let params = [ 75 ("repo", did.as_str()), 76 ("collection", collection), 77 ("rkey", &rkey), 78 ]; 79 - let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) 80 .query(&params) 81 .send() 82 .await 83 .expect("Failed to send get request"); 84 85 - assert_eq!(get_res.status(), StatusCode::OK, "Failed to get record after create"); 86 let get_body: Value = get_res.json().await.expect("get response was not JSON"); 87 assert_eq!(get_body["uri"], uri); 88 assert_eq!(get_body["value"]["text"], original_text); 89 - 90 91 let updated_text = "This post has been updated."; 92 let update_payload = json!({ ··· 100 } 101 }); 102 103 - let update_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 104 .bearer_auth(&jwt) 105 .json(&update_payload) 106 .send() 107 .await 108 .expect("Failed to send update request"); 109 110 - assert_eq!(update_res.status(), StatusCode::OK, "Failed to update record"); 111 112 - 113 - let get_updated_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) 114 .query(&params) 115 .send() 116 .await 117 .expect("Failed to send get-after-update request"); 118 119 - assert_eq!(get_updated_res.status(), StatusCode::OK, "Failed to get record after update"); 120 - let get_updated_body: Value = get_updated_res.json().await.expect("get-updated response was not JSON"); 121 - assert_eq!(get_updated_body["value"]["text"], updated_text, "Text was not updated"); 122 - 123 124 let delete_payload = json!({ 125 "repo": did, ··· 127 "rkey": rkey 128 }); 129 130 - let delete_res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await)) 131 .bearer_auth(&jwt) 132 .json(&delete_payload) 133 .send() 134 .await 135 .expect("Failed to send delete request"); 136 137 - assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete record"); 138 139 - 140 - let get_deleted_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) 141 .query(&params) 142 .send() 143 .await 144 .expect("Failed to send get-after-delete request"); 145 146 - assert_eq!(get_deleted_res.status(), StatusCode::NOT_FOUND, "Record was found, but it should be deleted"); 147 } 148 149 #[tokio::test] ··· 161 "displayName": "Original Name" 162 } 163 }); 164 - let create_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 165 .bearer_auth(&user_jwt) 166 .json(&profile_payload) 167 - .send().await.expect("create profile failed"); 168 169 - if create_res.status() != StatusCode::OK { 170 return; 171 } 172 173 - let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) 174 .query(&[ 175 ("repo", &user_did), 176 ("collection", &"app.bsky.actor.profile".to_string()), 177 ("rkey", &"self".to_string()), 178 ]) 179 - .send().await.expect("getRecord failed"); 180 let get_body: Value = get_res.json().await.expect("getRecord not json"); 181 - let cid_v1 = get_body["cid"].as_str().expect("Profile v1 had no CID").to_string(); 182 183 let update_payload_v2 = json!({ 184 "repo": user_did, ··· 190 }, 191 "swapCommit": cid_v1 // <-- Correctly point to v1 192 }); 193 - let update_res_v2 = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 194 .bearer_auth(&user_jwt) 195 .json(&update_payload_v2) 196 - .send().await.expect("putRecord v2 failed"); 197 - assert_eq!(update_res_v2.status(), StatusCode::OK, "v2 update failed"); 198 let update_body_v2: Value = update_res_v2.json().await.expect("v2 body not json"); 199 - let cid_v2 = update_body_v2["cid"].as_str().expect("v2 response had no CID").to_string(); 200 201 let update_payload_v3_stale = json!({ 202 "repo": user_did, ··· 208 }, 209 "swapCommit": cid_v1 210 }); 211 - let update_res_v3_stale = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 212 .bearer_auth(&user_jwt) 213 .json(&update_payload_v3_stale) 214 - .send().await.expect("putRecord v3 (stale) failed"); 215 216 assert_eq!( 217 update_res_v3_stale.status(), 218 - StatusCode::CONFLICT, 219 "Stale update did not cause a 409 Conflict" 220 ); 221 ··· 229 }, 230 "swapCommit": cid_v2 // <-- Correct 231 }); 232 - let update_res_v3_good = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 233 .bearer_auth(&user_jwt) 234 .json(&update_payload_v3_good) 235 - .send().await.expect("putRecord v3 (good) failed"); 236 237 - assert_eq!(update_res_v3_good.status(), StatusCode::OK, "v3 (good) update failed"); 238 }
··· 1 mod common; 2 use common::*; 3 4 use chrono::Utc; 5 + use reqwest; 6 + use serde_json::{Value, json}; 7 use std::time::Duration; 8 9 async fn setup_new_user(handle_prefix: &str) -> (String, String) { ··· 18 "email": email, 19 "password": password 20 }); 21 + let create_res = client 22 + .post(format!( 23 + "{}/xrpc/com.atproto.server.createAccount", 24 + base_url().await 25 + )) 26 .json(&create_account_payload) 27 .send() 28 .await 29 .expect("setup_new_user: Failed to send createAccount"); 30 31 + if create_res.status() != reqwest::StatusCode::OK { 32 + panic!( 33 + "setup_new_user: Failed to create account: {:?}", 34 + create_res.text().await 35 + ); 36 } 37 38 + let create_body: Value = create_res 39 + .json() 40 + .await 41 + .expect("setup_new_user: createAccount response was not JSON"); 42 43 + let new_did = create_body["did"] 44 + .as_str() 45 + .expect("setup_new_user: Response had no DID") 46 + .to_string(); 47 + let new_jwt = create_body["accessJwt"] 48 + .as_str() 49 + .expect("setup_new_user: Response had no accessJwt") 50 + .to_string(); 51 52 (new_did, new_jwt) 53 } ··· 74 } 75 }); 76 77 + let create_res = client 78 + .post(format!( 79 + "{}/xrpc/com.atproto.repo.putRecord", 80 + base_url().await 81 + )) 82 .bearer_auth(&jwt) 83 .json(&create_payload) 84 .send() 85 .await 86 .expect("Failed to send create request"); 87 88 + if create_res.status() != reqwest::StatusCode::OK { 89 + let status = create_res.status(); 90 + let body = create_res 91 + .text() 92 + .await 93 + .unwrap_or_else(|_| "Could not get body".to_string()); 94 + panic!( 95 + "Failed to create record. Status: {}, Body: {}", 96 + status, body 97 + ); 98 + } 99 100 + let create_body: Value = create_res 101 + .json() 102 + .await 103 + .expect("create response was not JSON"); 104 + let uri = create_body["uri"].as_str().unwrap(); 105 106 let params = [ 107 ("repo", did.as_str()), 108 ("collection", collection), 109 ("rkey", &rkey), 110 ]; 111 + let get_res = client 112 + .get(format!( 113 + "{}/xrpc/com.atproto.repo.getRecord", 114 + base_url().await 115 + )) 116 .query(&params) 117 .send() 118 .await 119 .expect("Failed to send get request"); 120 121 + assert_eq!( 122 + get_res.status(), 123 + reqwest::StatusCode::OK, 124 + "Failed to get record after create" 125 + ); 126 let get_body: Value = get_res.json().await.expect("get response was not JSON"); 127 assert_eq!(get_body["uri"], uri); 128 assert_eq!(get_body["value"]["text"], original_text); 129 130 let updated_text = "This post has been updated."; 131 let update_payload = json!({ ··· 139 } 140 }); 141 142 + let update_res = client 143 + .post(format!( 144 + "{}/xrpc/com.atproto.repo.putRecord", 145 + base_url().await 146 + )) 147 .bearer_auth(&jwt) 148 .json(&update_payload) 149 .send() 150 .await 151 .expect("Failed to send update request"); 152 153 + assert_eq!( 154 + update_res.status(), 155 + reqwest::StatusCode::OK, 156 + "Failed to update record" 157 + ); 158 159 + let get_updated_res = client 160 + .get(format!( 161 + "{}/xrpc/com.atproto.repo.getRecord", 162 + base_url().await 163 + )) 164 .query(&params) 165 .send() 166 .await 167 .expect("Failed to send get-after-update request"); 168 169 + assert_eq!( 170 + get_updated_res.status(), 171 + reqwest::StatusCode::OK, 172 + "Failed to get record after update" 173 + ); 174 + let get_updated_body: Value = get_updated_res 175 + .json() 176 + .await 177 + .expect("get-updated response was not JSON"); 178 + assert_eq!( 179 + get_updated_body["value"]["text"], updated_text, 180 + "Text was not updated" 181 + ); 182 183 let delete_payload = json!({ 184 "repo": did, ··· 186 "rkey": rkey 187 }); 188 189 + let delete_res = client 190 + .post(format!( 191 + "{}/xrpc/com.atproto.repo.deleteRecord", 192 + base_url().await 193 + )) 194 .bearer_auth(&jwt) 195 .json(&delete_payload) 196 .send() 197 .await 198 .expect("Failed to send delete request"); 199 200 + assert_eq!( 201 + delete_res.status(), 202 + reqwest::StatusCode::OK, 203 + "Failed to delete record" 204 + ); 205 206 + let get_deleted_res = client 207 + .get(format!( 208 + "{}/xrpc/com.atproto.repo.getRecord", 209 + base_url().await 210 + )) 211 .query(&params) 212 .send() 213 .await 214 .expect("Failed to send get-after-delete request"); 215 216 + assert_eq!( 217 + get_deleted_res.status(), 218 + reqwest::StatusCode::NOT_FOUND, 219 + "Record was found, but it should be deleted" 220 + ); 221 } 222 223 #[tokio::test] ··· 235 "displayName": "Original Name" 236 } 237 }); 238 + let create_res = client 239 + .post(format!( 240 + "{}/xrpc/com.atproto.repo.putRecord", 241 + base_url().await 242 + )) 243 .bearer_auth(&user_jwt) 244 .json(&profile_payload) 245 + .send() 246 + .await 247 + .expect("create profile failed"); 248 249 + if create_res.status() != reqwest::StatusCode::OK { 250 return; 251 } 252 253 + let get_res = client 254 + .get(format!( 255 + "{}/xrpc/com.atproto.repo.getRecord", 256 + base_url().await 257 + )) 258 .query(&[ 259 ("repo", &user_did), 260 ("collection", &"app.bsky.actor.profile".to_string()), 261 ("rkey", &"self".to_string()), 262 ]) 263 + .send() 264 + .await 265 + .expect("getRecord failed"); 266 let get_body: Value = get_res.json().await.expect("getRecord not json"); 267 + let cid_v1 = get_body["cid"] 268 + .as_str() 269 + .expect("Profile v1 had no CID") 270 + .to_string(); 271 272 let update_payload_v2 = json!({ 273 "repo": user_did, ··· 279 }, 280 "swapCommit": cid_v1 // <-- Correctly point to v1 281 }); 282 + let update_res_v2 = client 283 + .post(format!( 284 + "{}/xrpc/com.atproto.repo.putRecord", 285 + base_url().await 286 + )) 287 .bearer_auth(&user_jwt) 288 .json(&update_payload_v2) 289 + .send() 290 + .await 291 + .expect("putRecord v2 failed"); 292 + assert_eq!( 293 + update_res_v2.status(), 294 + reqwest::StatusCode::OK, 295 + "v2 update failed" 296 + ); 297 let update_body_v2: Value = update_res_v2.json().await.expect("v2 body not json"); 298 + let cid_v2 = update_body_v2["cid"] 299 + .as_str() 300 + .expect("v2 response had no CID") 301 + .to_string(); 302 303 let update_payload_v3_stale = json!({ 304 "repo": user_did, ··· 310 }, 311 "swapCommit": cid_v1 312 }); 313 + let update_res_v3_stale = client 314 + .post(format!( 315 + "{}/xrpc/com.atproto.repo.putRecord", 316 + base_url().await 317 + )) 318 .bearer_auth(&user_jwt) 319 .json(&update_payload_v3_stale) 320 + .send() 321 + .await 322 + .expect("putRecord v3 (stale) failed"); 323 324 assert_eq!( 325 update_res_v3_stale.status(), 326 + reqwest::StatusCode::CONFLICT, 327 "Stale update did not cause a 409 Conflict" 328 ); 329 ··· 337 }, 338 "swapCommit": cid_v2 // <-- Correct 339 }); 340 + let update_res_v3_good = client 341 + .post(format!( 342 + "{}/xrpc/com.atproto.repo.putRecord", 343 + base_url().await 344 + )) 345 .bearer_auth(&user_jwt) 346 .json(&update_payload_v3_good) 347 + .send() 348 + .await 349 + .expect("putRecord v3 (good) failed"); 350 + 351 + assert_eq!( 352 + update_res_v3_good.status(), 353 + reqwest::StatusCode::OK, 354 + "v3 (good) update failed" 355 + ); 356 + } 357 + 358 + async fn create_post( 359 + client: &reqwest::Client, 360 + did: &str, 361 + jwt: &str, 362 + text: &str, 363 + ) -> (String, String) { 364 + let collection = "app.bsky.feed.post"; 365 + let rkey = format!("e2e_social_{}", Utc::now().timestamp_millis()); 366 + let now = Utc::now().to_rfc3339(); 367 + 368 + let create_payload = json!({ 369 + "repo": did, 370 + "collection": collection, 371 + "rkey": rkey, 372 + "record": { 373 + "$type": collection, 374 + "text": text, 375 + "createdAt": now 376 + } 377 + }); 378 + 379 + let create_res = client 380 + .post(format!( 381 + "{}/xrpc/com.atproto.repo.putRecord", 382 + base_url().await 383 + )) 384 + .bearer_auth(jwt) 385 + .json(&create_payload) 386 + .send() 387 + .await 388 + .expect("Failed to send create post request"); 389 + 390 + assert_eq!( 391 + create_res.status(), 392 + reqwest::StatusCode::OK, 393 + "Failed to create post record" 394 + ); 395 + let create_body: Value = create_res 396 + .json() 397 + .await 398 + .expect("create post response was not JSON"); 399 + let uri = create_body["uri"].as_str().unwrap().to_string(); 400 + let cid = create_body["cid"].as_str().unwrap().to_string(); 401 + (uri, cid) 402 + } 403 + 404 + async fn create_follow( 405 + client: &reqwest::Client, 406 + follower_did: &str, 407 + follower_jwt: &str, 408 + followee_did: &str, 409 + ) -> (String, String) { 410 + let collection = "app.bsky.graph.follow"; 411 + let rkey = format!("e2e_follow_{}", Utc::now().timestamp_millis()); 412 + let now = Utc::now().to_rfc3339(); 413 + 414 + let create_payload = json!({ 415 + "repo": follower_did, 416 + "collection": collection, 417 + "rkey": rkey, 418 + "record": { 419 + "$type": collection, 420 + "subject": followee_did, 421 + "createdAt": now 422 + } 423 + }); 424 425 + let create_res = client 426 + .post(format!( 427 + "{}/xrpc/com.atproto.repo.putRecord", 428 + base_url().await 429 + )) 430 + .bearer_auth(follower_jwt) 431 + .json(&create_payload) 432 + .send() 433 + .await 434 + .expect("Failed to send create follow request"); 435 + 436 + assert_eq!( 437 + create_res.status(), 438 + reqwest::StatusCode::OK, 439 + "Failed to create follow record" 440 + ); 441 + let create_body: Value = create_res 442 + .json() 443 + .await 444 + .expect("create follow response was not JSON"); 445 + let uri = create_body["uri"].as_str().unwrap().to_string(); 446 + let cid = create_body["cid"].as_str().unwrap().to_string(); 447 + (uri, cid) 448 + } 449 + 450 + #[tokio::test] 451 + #[ignore] 452 + async fn test_social_flow_lifecycle() { 453 + let client = client(); 454 + 455 + let (alice_did, alice_jwt) = setup_new_user("alice-social").await; 456 + let (bob_did, bob_jwt) = setup_new_user("bob-social").await; 457 + 458 + let (post1_uri, _) = create_post(&client, &alice_did, &alice_jwt, "Alice's first post!").await; 459 + 460 + create_follow(&client, &bob_did, &bob_jwt, &alice_did).await; 461 + 462 + tokio::time::sleep(Duration::from_secs(1)).await; 463 + 464 + let timeline_res_1 = client 465 + .get(format!( 466 + "{}/xrpc/app.bsky.feed.getTimeline", 467 + base_url().await 468 + )) 469 + .bearer_auth(&bob_jwt) 470 + .send() 471 + .await 472 + .expect("Failed to get timeline (1)"); 473 + 474 + assert_eq!( 475 + timeline_res_1.status(), 476 + reqwest::StatusCode::OK, 477 + "Failed to get timeline (1)" 478 + ); 479 + let timeline_body_1: Value = timeline_res_1.json().await.expect("Timeline (1) not JSON"); 480 + let feed_1 = timeline_body_1["feed"].as_array().unwrap(); 481 + assert_eq!(feed_1.len(), 1, "Timeline should have 1 post"); 482 + assert_eq!( 483 + feed_1[0]["post"]["uri"], post1_uri, 484 + "Post URI mismatch in timeline (1)" 485 + ); 486 + 487 + let (post2_uri, _) = create_post( 488 + &client, 489 + &alice_did, 490 + &alice_jwt, 491 + "Alice's second post, so exciting!", 492 + ) 493 + .await; 494 + 495 + tokio::time::sleep(Duration::from_secs(1)).await; 496 + 497 + let timeline_res_2 = client 498 + .get(format!( 499 + "{}/xrpc/app.bsky.feed.getTimeline", 500 + base_url().await 501 + )) 502 + .bearer_auth(&bob_jwt) 503 + .send() 504 + .await 505 + .expect("Failed to get timeline (2)"); 506 + 507 + assert_eq!( 508 + timeline_res_2.status(), 509 + reqwest::StatusCode::OK, 510 + "Failed to get timeline (2)" 511 + ); 512 + let timeline_body_2: Value = timeline_res_2.json().await.expect("Timeline (2) not JSON"); 513 + let feed_2 = timeline_body_2["feed"].as_array().unwrap(); 514 + assert_eq!(feed_2.len(), 2, "Timeline should have 2 posts"); 515 + assert_eq!( 516 + feed_2[0]["post"]["uri"], post2_uri, 517 + "Post 2 should be first" 518 + ); 519 + assert_eq!( 520 + feed_2[1]["post"]["uri"], post1_uri, 521 + "Post 1 should be second" 522 + ); 523 + 524 + let delete_payload = json!({ 525 + "repo": alice_did, 526 + "collection": "app.bsky.feed.post", 527 + "rkey": post1_uri.split('/').last().unwrap() 528 + }); 529 + let delete_res = client 530 + .post(format!( 531 + "{}/xrpc/com.atproto.repo.deleteRecord", 532 + base_url().await 533 + )) 534 + .bearer_auth(&alice_jwt) 535 + .json(&delete_payload) 536 + .send() 537 + .await 538 + .expect("Failed to send delete request"); 539 + assert_eq!( 540 + delete_res.status(), 541 + reqwest::StatusCode::OK, 542 + "Failed to delete record" 543 + ); 544 + 545 + tokio::time::sleep(Duration::from_secs(1)).await; 546 + 547 + let timeline_res_3 = client 548 + .get(format!( 549 + "{}/xrpc/app.bsky.feed.getTimeline", 550 + base_url().await 551 + )) 552 + .bearer_auth(&bob_jwt) 553 + .send() 554 + .await 555 + .expect("Failed to get timeline (3)"); 556 + 557 + assert_eq!( 558 + timeline_res_3.status(), 559 + reqwest::StatusCode::OK, 560 + "Failed to get timeline (3)" 561 + ); 562 + let timeline_body_3: Value = timeline_res_3.json().await.expect("Timeline (3) not JSON"); 563 + let feed_3 = timeline_body_3["feed"].as_array().unwrap(); 564 + assert_eq!(feed_3.len(), 1, "Timeline should have 1 post after delete"); 565 + assert_eq!( 566 + feed_3[0]["post"]["uri"], post2_uri, 567 + "Only post 2 should remain" 568 + ); 569 }
+24 -16
tests/proxy.rs
··· 1 mod common; 2 3 - use axum::{ 4 - routing::any, 5 - Router, 6 - extract::Request, 7 - http::StatusCode, 8 - }; 9 - use tokio::net::TcpListener; 10 use reqwest::Client; 11 use std::sync::Arc; 12 - use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 13 14 - async fn spawn_mock_upstream() -> (String, tokio::sync::mpsc::Receiver<(String, String, Option<String>)>) { 15 let (tx, rx) = tokio::sync::mpsc::channel(10); 16 let tx = Arc::new(tx); 17 ··· 20 async move { 21 let method = req.method().to_string(); 22 let uri = req.uri().to_string(); 23 - let auth = req.headers().get("Authorization") 24 .and_then(|h| h.to_str().ok()) 25 .map(|s| s.to_string()); 26 ··· 45 let (upstream_url, mut rx) = spawn_mock_upstream().await; 46 let client = Client::new(); 47 48 - let res = client.get(format!("{}/xrpc/com.example.test", app_url)) 49 .header("atproto-proxy", &upstream_url) 50 .header("Authorization", "Bearer test-token") 51 .send() ··· 65 async fn test_proxy_via_env_var() { 66 let (upstream_url, mut rx) = spawn_mock_upstream().await; 67 68 - unsafe { std::env::set_var("APPVIEW_URL", &upstream_url); } 69 70 let app_url = common::base_url().await; 71 let client = Client::new(); 72 73 - let res = client.get(format!("{}/xrpc/com.example.envtest", app_url)) 74 .send() 75 .await 76 .unwrap(); ··· 85 #[tokio::test] 86 #[ignore] 87 async fn test_proxy_missing_config() { 88 - unsafe { std::env::remove_var("APPVIEW_URL"); } 89 90 let app_url = common::base_url().await; 91 let client = Client::new(); 92 93 - let res = client.get(format!("{}/xrpc/com.example.fail", app_url)) 94 .send() 95 .await 96 .unwrap(); ··· 106 107 let (access_jwt, did) = common::create_account_and_login(&client).await; 108 109 - let res = client.get(format!("{}/xrpc/com.example.signed", app_url)) 110 .header("atproto-proxy", &upstream_url) 111 .header("Authorization", format!("Bearer {}", access_jwt)) 112 .send()
··· 1 mod common; 2 3 + use axum::{Router, extract::Request, http::StatusCode, routing::any}; 4 + use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 5 use reqwest::Client; 6 use std::sync::Arc; 7 + use tokio::net::TcpListener; 8 9 + async fn spawn_mock_upstream() -> ( 10 + String, 11 + tokio::sync::mpsc::Receiver<(String, String, Option<String>)>, 12 + ) { 13 let (tx, rx) = tokio::sync::mpsc::channel(10); 14 let tx = Arc::new(tx); 15 ··· 18 async move { 19 let method = req.method().to_string(); 20 let uri = req.uri().to_string(); 21 + let auth = req 22 + .headers() 23 + .get("Authorization") 24 .and_then(|h| h.to_str().ok()) 25 .map(|s| s.to_string()); 26 ··· 45 let (upstream_url, mut rx) = spawn_mock_upstream().await; 46 let client = Client::new(); 47 48 + let res = client 49 + .get(format!("{}/xrpc/com.example.test", app_url)) 50 .header("atproto-proxy", &upstream_url) 51 .header("Authorization", "Bearer test-token") 52 .send() ··· 66 async fn test_proxy_via_env_var() { 67 let (upstream_url, mut rx) = spawn_mock_upstream().await; 68 69 + unsafe { 70 + std::env::set_var("APPVIEW_URL", &upstream_url); 71 + } 72 73 let app_url = common::base_url().await; 74 let client = Client::new(); 75 76 + let res = client 77 + .get(format!("{}/xrpc/com.example.envtest", app_url)) 78 .send() 79 .await 80 .unwrap(); ··· 89 #[tokio::test] 90 #[ignore] 91 async fn test_proxy_missing_config() { 92 + unsafe { 93 + std::env::remove_var("APPVIEW_URL"); 94 + } 95 96 let app_url = common::base_url().await; 97 let client = Client::new(); 98 99 + let res = client 100 + .get(format!("{}/xrpc/com.example.fail", app_url)) 101 .send() 102 .await 103 .unwrap(); ··· 113 114 let (access_jwt, did) = common::create_account_and_login(&client).await; 115 116 + let res = client 117 + .get(format!("{}/xrpc/com.example.signed", app_url)) 118 .header("atproto-proxy", &upstream_url) 119 .header("Authorization", format!("Bearer {}", access_jwt)) 120 .send()
+103 -28
tests/repo.rs
··· 1 mod common; 2 use common::*; 3 4 - use reqwest::{header, StatusCode}; 5 - use serde_json::{json, Value}; 6 use chrono::Utc; 7 8 #[tokio::test] 9 #[ignore] ··· 15 ("rkey", "self"), 16 ]; 17 18 - let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) 19 .query(&params) 20 .send() 21 .await ··· 36 ("rkey", "nonexistent"), 37 ]; 38 39 - let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) 40 .query(&params) 41 .send() 42 .await ··· 50 #[tokio::test] 51 async fn test_upload_blob_no_auth() { 52 let client = client(); 53 - let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await)) 54 .header(header::CONTENT_TYPE, "text/plain") 55 .body("no auth") 56 .send() ··· 66 async fn test_upload_blob_success() { 67 let client = client(); 68 let (token, _) = create_account_and_login(&client).await; 69 - let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await)) 70 .header(header::CONTENT_TYPE, "text/plain") 71 .bearer_auth(token) 72 .body("This is our blob data") ··· 90 "record": {} 91 }); 92 93 - let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 94 .json(&payload) 95 .send() 96 .await ··· 118 } 119 }); 120 121 - let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 122 .bearer_auth(token) 123 .json(&payload) 124 .send() ··· 135 #[ignore] 136 async fn test_get_record_missing_params() { 137 let client = client(); 138 - let params = [ 139 - ("repo", "did:plc:12345"), 140 - ]; 141 142 - let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) 143 .query(&params) 144 .send() 145 .await 146 .expect("Failed to send request"); 147 148 - assert_eq!(res.status(), StatusCode::BAD_REQUEST, "Expected 400 for missing params"); 149 } 150 151 #[tokio::test] 152 async fn test_upload_blob_bad_token() { 153 let client = client(); 154 - let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await)) 155 .header(header::CONTENT_TYPE, "text/plain") 156 .bearer_auth(BAD_AUTH_TOKEN) 157 .body("This is our blob data") ··· 181 } 182 }); 183 184 - let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 185 .bearer_auth(token) 186 .json(&payload) 187 .send() 188 .await 189 .expect("Failed to send request"); 190 191 - assert_eq!(res.status(), StatusCode::FORBIDDEN, "Expected 403 for mismatched repo and auth"); 192 } 193 194 #[tokio::test] ··· 207 } 208 }); 209 210 - let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 211 .bearer_auth(token) 212 .json(&payload) 213 .send() 214 .await 215 .expect("Failed to send request"); 216 217 - assert_eq!(res.status(), StatusCode::BAD_REQUEST, "Expected 400 for invalid record schema"); 218 } 219 220 #[tokio::test] 221 async fn test_upload_blob_unsupported_mime_type() { 222 let client = client(); 223 let (token, _) = create_account_and_login(&client).await; 224 - let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await)) 225 .header(header::CONTENT_TYPE, "application/xml") 226 .bearer_auth(token) 227 .body("<xml>not an image</xml>") ··· 242 ("collection", "app.bsky.feed.post"), 243 ("limit", "10"), 244 ]; 245 - let res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", base_url().await)) 246 .query(&params) 247 .send() 248 .await ··· 255 async fn test_describe_repo() { 256 let client = client(); 257 let (_, did) = create_account_and_login(&client).await; 258 - let params = [ 259 - ("repo", did.as_str()), 260 - ]; 261 - let res = client.get(format!("{}/xrpc/com.atproto.repo.describeRepo", base_url().await)) 262 .query(&params) 263 .send() 264 .await ··· 282 } 283 }); 284 285 - let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await)) 286 .json(&payload) 287 .bearer_auth(token) 288 .send() ··· 313 } 314 }); 315 316 - let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await)) 317 .json(&payload) 318 .bearer_auth(token) 319 .send() ··· 322 323 assert_eq!(res.status(), StatusCode::OK); 324 let body: Value = res.json().await.expect("Response was not valid JSON"); 325 - assert_eq!(body["uri"], format!("at://{}/app.bsky.feed.post/{}", did, rkey)); 326 // assert_eq!(body["cid"], "bafyreihy"); 327 } 328 ··· 336 "collection": "app.bsky.feed.post", 337 "rkey": "some_post_to_delete" 338 }); 339 - let res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await)) 340 .bearer_auth(token) 341 .json(&payload) 342 .send()
··· 1 mod common; 2 use common::*; 3 4 use chrono::Utc; 5 + use reqwest::{StatusCode, header}; 6 + use serde_json::{Value, json}; 7 8 #[tokio::test] 9 #[ignore] ··· 15 ("rkey", "self"), 16 ]; 17 18 + let res = client 19 + .get(format!( 20 + "{}/xrpc/com.atproto.repo.getRecord", 21 + base_url().await 22 + )) 23 .query(&params) 24 .send() 25 .await ··· 40 ("rkey", "nonexistent"), 41 ]; 42 43 + let res = client 44 + .get(format!( 45 + "{}/xrpc/com.atproto.repo.getRecord", 46 + base_url().await 47 + )) 48 .query(&params) 49 .send() 50 .await ··· 58 #[tokio::test] 59 async fn test_upload_blob_no_auth() { 60 let client = client(); 61 + let res = client 62 + .post(format!( 63 + "{}/xrpc/com.atproto.repo.uploadBlob", 64 + base_url().await 65 + )) 66 .header(header::CONTENT_TYPE, "text/plain") 67 .body("no auth") 68 .send() ··· 78 async fn test_upload_blob_success() { 79 let client = client(); 80 let (token, _) = create_account_and_login(&client).await; 81 + let res = client 82 + .post(format!( 83 + "{}/xrpc/com.atproto.repo.uploadBlob", 84 + base_url().await 85 + )) 86 .header(header::CONTENT_TYPE, "text/plain") 87 .bearer_auth(token) 88 .body("This is our blob data") ··· 106 "record": {} 107 }); 108 109 + let res = client 110 + .post(format!( 111 + "{}/xrpc/com.atproto.repo.putRecord", 112 + base_url().await 113 + )) 114 .json(&payload) 115 .send() 116 .await ··· 138 } 139 }); 140 141 + let res = client 142 + .post(format!( 143 + "{}/xrpc/com.atproto.repo.putRecord", 144 + base_url().await 145 + )) 146 .bearer_auth(token) 147 .json(&payload) 148 .send() ··· 159 #[ignore] 160 async fn test_get_record_missing_params() { 161 let client = client(); 162 + let params = [("repo", "did:plc:12345")]; 163 164 + let res = client 165 + .get(format!( 166 + "{}/xrpc/com.atproto.repo.getRecord", 167 + base_url().await 168 + )) 169 .query(&params) 170 .send() 171 .await 172 .expect("Failed to send request"); 173 174 + assert_eq!( 175 + res.status(), 176 + StatusCode::BAD_REQUEST, 177 + "Expected 400 for missing params" 178 + ); 179 } 180 181 #[tokio::test] 182 async fn test_upload_blob_bad_token() { 183 let client = client(); 184 + let res = client 185 + .post(format!( 186 + "{}/xrpc/com.atproto.repo.uploadBlob", 187 + base_url().await 188 + )) 189 .header(header::CONTENT_TYPE, "text/plain") 190 .bearer_auth(BAD_AUTH_TOKEN) 191 .body("This is our blob data") ··· 215 } 216 }); 217 218 + let res = client 219 + .post(format!( 220 + "{}/xrpc/com.atproto.repo.putRecord", 221 + base_url().await 222 + )) 223 .bearer_auth(token) 224 .json(&payload) 225 .send() 226 .await 227 .expect("Failed to send request"); 228 229 + assert_eq!( 230 + res.status(), 231 + StatusCode::FORBIDDEN, 232 + "Expected 403 for mismatched repo and auth" 233 + ); 234 } 235 236 #[tokio::test] ··· 249 } 250 }); 251 252 + let res = client 253 + .post(format!( 254 + "{}/xrpc/com.atproto.repo.putRecord", 255 + base_url().await 256 + )) 257 .bearer_auth(token) 258 .json(&payload) 259 .send() 260 .await 261 .expect("Failed to send request"); 262 263 + assert_eq!( 264 + res.status(), 265 + StatusCode::BAD_REQUEST, 266 + "Expected 400 for invalid record schema" 267 + ); 268 } 269 270 #[tokio::test] 271 async fn test_upload_blob_unsupported_mime_type() { 272 let client = client(); 273 let (token, _) = create_account_and_login(&client).await; 274 + let res = client 275 + .post(format!( 276 + "{}/xrpc/com.atproto.repo.uploadBlob", 277 + base_url().await 278 + )) 279 .header(header::CONTENT_TYPE, "application/xml") 280 .bearer_auth(token) 281 .body("<xml>not an image</xml>") ··· 296 ("collection", "app.bsky.feed.post"), 297 ("limit", "10"), 298 ]; 299 + let res = client 300 + .get(format!( 301 + "{}/xrpc/com.atproto.repo.listRecords", 302 + base_url().await 303 + )) 304 .query(&params) 305 .send() 306 .await ··· 313 async fn test_describe_repo() { 314 let client = client(); 315 let (_, did) = create_account_and_login(&client).await; 316 + let params = [("repo", did.as_str())]; 317 + let res = client 318 + .get(format!( 319 + "{}/xrpc/com.atproto.repo.describeRepo", 320 + base_url().await 321 + )) 322 .query(&params) 323 .send() 324 .await ··· 342 } 343 }); 344 345 + let res = client 346 + .post(format!( 347 + "{}/xrpc/com.atproto.repo.createRecord", 348 + base_url().await 349 + )) 350 .json(&payload) 351 .bearer_auth(token) 352 .send() ··· 377 } 378 }); 379 380 + let res = client 381 + .post(format!( 382 + "{}/xrpc/com.atproto.repo.createRecord", 383 + base_url().await 384 + )) 385 .json(&payload) 386 .bearer_auth(token) 387 .send() ··· 390 391 assert_eq!(res.status(), StatusCode::OK); 392 let body: Value = res.json().await.expect("Response was not valid JSON"); 393 + assert_eq!( 394 + body["uri"], 395 + format!("at://{}/app.bsky.feed.post/{}", did, rkey) 396 + ); 397 // assert_eq!(body["cid"], "bafyreihy"); 398 } 399 ··· 407 "collection": "app.bsky.feed.post", 408 "rkey": "some_post_to_delete" 409 }); 410 + let res = client 411 + .post(format!( 412 + "{}/xrpc/com.atproto.repo.deleteRecord", 413 + base_url().await 414 + )) 415 .bearer_auth(token) 416 .json(&payload) 417 .send()
+71 -17
tests/server.rs
··· 2 use common::*; 3 4 use reqwest::StatusCode; 5 - use serde_json::{json, Value}; 6 7 #[tokio::test] 8 async fn test_health() { 9 let client = client(); 10 - let res = client.get(format!("{}/health", base_url().await)) 11 .send() 12 .await 13 .expect("Failed to send request"); ··· 19 #[tokio::test] 20 async fn test_describe_server() { 21 let client = client(); 22 - let res = client.get(format!("{}/xrpc/com.atproto.server.describeServer", base_url().await)) 23 .send() 24 .await 25 .expect("Failed to send request"); ··· 39 "email": format!("{}@example.com", handle), 40 "password": "password" 41 }); 42 - let _ = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await)) 43 .json(&payload) 44 .send() 45 .await; ··· 49 "password": "password" 50 }); 51 52 - let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", base_url().await)) 53 .json(&payload) 54 .send() 55 .await ··· 67 "password": "password" 68 }); 69 70 - let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", base_url().await)) 71 .json(&payload) 72 .send() 73 .await 74 .expect("Failed to send request"); 75 76 - assert!(res.status() == StatusCode::BAD_REQUEST || res.status() == StatusCode::UNPROCESSABLE_ENTITY, 77 - "Expected 400 or 422 for missing identifier, got {}", res.status()); 78 } 79 80 #[tokio::test] ··· 86 "password": "password" 87 }); 88 89 - let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await)) 90 .json(&payload) 91 .send() 92 .await 93 .expect("Failed to send request"); 94 95 - assert_eq!(res.status(), StatusCode::BAD_REQUEST, "Expected 400 for invalid handle chars"); 96 } 97 98 #[tokio::test] 99 async fn test_get_session() { 100 let client = client(); 101 - let res = client.get(format!("{}/xrpc/com.atproto.server.getSession", base_url().await)) 102 .bearer_auth(AUTH_TOKEN) 103 .send() 104 .await ··· 117 "email": format!("{}@example.com", handle), 118 "password": "password" 119 }); 120 - let _ = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await)) 121 .json(&payload) 122 .send() 123 .await; ··· 126 "identifier": handle, 127 "password": "password" 128 }); 129 - let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", base_url().await)) 130 .json(&login_payload) 131 .send() 132 .await ··· 134 135 assert_eq!(res.status(), StatusCode::OK); 136 let body: Value = res.json().await.expect("Invalid JSON"); 137 - let refresh_jwt = body["refreshJwt"].as_str().expect("No refreshJwt").to_string(); 138 - let access_jwt = body["accessJwt"].as_str().expect("No accessJwt").to_string(); 139 140 - let res = client.post(format!("{}/xrpc/com.atproto.server.refreshSession", base_url().await)) 141 .bearer_auth(&refresh_jwt) 142 .send() 143 .await ··· 154 #[tokio::test] 155 async fn test_delete_session() { 156 let client = client(); 157 - let res = client.post(format!("{}/xrpc/com.atproto.server.deleteSession", base_url().await)) 158 .bearer_auth(AUTH_TOKEN) 159 .send() 160 .await
··· 2 use common::*; 3 4 use reqwest::StatusCode; 5 + use serde_json::{Value, json}; 6 7 #[tokio::test] 8 async fn test_health() { 9 let client = client(); 10 + let res = client 11 + .get(format!("{}/health", base_url().await)) 12 .send() 13 .await 14 .expect("Failed to send request"); ··· 20 #[tokio::test] 21 async fn test_describe_server() { 22 let client = client(); 23 + let res = client 24 + .get(format!( 25 + "{}/xrpc/com.atproto.server.describeServer", 26 + base_url().await 27 + )) 28 .send() 29 .await 30 .expect("Failed to send request"); ··· 44 "email": format!("{}@example.com", handle), 45 "password": "password" 46 }); 47 + let _ = client 48 + .post(format!( 49 + "{}/xrpc/com.atproto.server.createAccount", 50 + base_url().await 51 + )) 52 .json(&payload) 53 .send() 54 .await; ··· 58 "password": "password" 59 }); 60 61 + let res = client 62 + .post(format!( 63 + "{}/xrpc/com.atproto.server.createSession", 64 + base_url().await 65 + )) 66 .json(&payload) 67 .send() 68 .await ··· 80 "password": "password" 81 }); 82 83 + let res = client 84 + .post(format!( 85 + "{}/xrpc/com.atproto.server.createSession", 86 + base_url().await 87 + )) 88 .json(&payload) 89 .send() 90 .await 91 .expect("Failed to send request"); 92 93 + assert!( 94 + res.status() == StatusCode::BAD_REQUEST || res.status() == StatusCode::UNPROCESSABLE_ENTITY, 95 + "Expected 400 or 422 for missing identifier, got {}", 96 + res.status() 97 + ); 98 } 99 100 #[tokio::test] ··· 106 "password": "password" 107 }); 108 109 + let res = client 110 + .post(format!( 111 + "{}/xrpc/com.atproto.server.createAccount", 112 + base_url().await 113 + )) 114 .json(&payload) 115 .send() 116 .await 117 .expect("Failed to send request"); 118 119 + assert_eq!( 120 + res.status(), 121 + StatusCode::BAD_REQUEST, 122 + "Expected 400 for invalid handle chars" 123 + ); 124 } 125 126 #[tokio::test] 127 async fn test_get_session() { 128 let client = client(); 129 + let res = client 130 + .get(format!( 131 + "{}/xrpc/com.atproto.server.getSession", 132 + base_url().await 133 + )) 134 .bearer_auth(AUTH_TOKEN) 135 .send() 136 .await ··· 149 "email": format!("{}@example.com", handle), 150 "password": "password" 151 }); 152 + let _ = client 153 + .post(format!( 154 + "{}/xrpc/com.atproto.server.createAccount", 155 + base_url().await 156 + )) 157 .json(&payload) 158 .send() 159 .await; ··· 162 "identifier": handle, 163 "password": "password" 164 }); 165 + let res = client 166 + .post(format!( 167 + "{}/xrpc/com.atproto.server.createSession", 168 + base_url().await 169 + )) 170 .json(&login_payload) 171 .send() 172 .await ··· 174 175 assert_eq!(res.status(), StatusCode::OK); 176 let body: Value = res.json().await.expect("Invalid JSON"); 177 + let refresh_jwt = body["refreshJwt"] 178 + .as_str() 179 + .expect("No refreshJwt") 180 + .to_string(); 181 + let access_jwt = body["accessJwt"] 182 + .as_str() 183 + .expect("No accessJwt") 184 + .to_string(); 185 186 + let res = client 187 + .post(format!( 188 + "{}/xrpc/com.atproto.server.refreshSession", 189 + base_url().await 190 + )) 191 .bearer_auth(&refresh_jwt) 192 .send() 193 .await ··· 204 #[tokio::test] 205 async fn test_delete_session() { 206 let client = client(); 207 + let res = client 208 + .post(format!( 209 + "{}/xrpc/com.atproto.server.deleteSession", 210 + base_url().await 211 + )) 212 .bearer_auth(AUTH_TOKEN) 213 .send() 214 .await
+11 -5
tests/sync.rs
··· 6 #[ignore] 7 async fn test_get_repo() { 8 let client = client(); 9 - let params = [ 10 - ("did", AUTH_DID), 11 - ]; 12 - let res = client.get(format!("{}/xrpc/com.atproto.sync.getRepo", base_url().await)) 13 .query(&params) 14 .send() 15 .await ··· 26 ("did", AUTH_DID), 27 // "cids" would be a list of CIDs 28 ]; 29 - let res = client.get(format!("{}/xrpc/com.atproto.sync.getBlocks", base_url().await)) 30 .query(&params) 31 .send() 32 .await
··· 6 #[ignore] 7 async fn test_get_repo() { 8 let client = client(); 9 + let params = [("did", AUTH_DID)]; 10 + let res = client 11 + .get(format!( 12 + "{}/xrpc/com.atproto.sync.getRepo", 13 + base_url().await 14 + )) 15 .query(&params) 16 .send() 17 .await ··· 28 ("did", AUTH_DID), 29 // "cids" would be a list of CIDs 30 ]; 31 + let res = client 32 + .get(format!( 33 + "{}/xrpc/com.atproto.sync.getBlocks", 34 + base_url().await 35 + )) 36 .query(&params) 37 .send() 38 .await