An easy-to-host PDS on the ATProtocol, iPhone and MacOS. Maintain control of your keys and data, always.

refactor(relay): eliminate DRY violations across admin route handlers

Extract shared admin Bearer token validation, code generation, and test
helpers that were duplicated across claim_codes, create_account, and
create_signing_key.

- routes/auth.rs: require_admin_token() replaces 37-line auth block copied 3×;
also fixes create_signing_key which was missing the inspect_err debug log
for non-UTF-8 Authorization headers
- routes/code_gen.rs: generate_code() + CODE_LEN/CHARSET moved here from
claim_codes and create_account where they were defined identically
- routes/test_utils.rs: test_state_with_admin_token() shared instead of
duplicated in each route's test module
- create_account: consolidate 4 separate pre-check queries into 2 OR EXISTS
queries (email and handle each check both tables in one round-trip)

+121 -187
+52
crates/relay/src/routes/auth.rs
··· 1 + use axum::http::HeaderMap; 2 + use subtle::ConstantTimeEq; 3 + 4 + use common::{ApiError, ErrorCode}; 5 + 6 + use crate::app::AppState; 7 + 8 + /// Validate the admin Bearer token from request headers. 9 + /// 10 + /// Returns `Ok(())` when the token is present, has the `"Bearer "` prefix, and matches 11 + /// `Config.admin_token` in constant time. Returns `ApiError::Unauthorized` in all other 12 + /// cases, including when the server has no token configured. 13 + /// 14 + /// Call this at the top of any handler that requires admin access. 15 + pub fn require_admin_token(headers: &HeaderMap, state: &AppState) -> Result<(), ApiError> { 16 + let expected_token = state 17 + .config 18 + .admin_token 19 + .as_deref() 20 + .ok_or_else(|| ApiError::new(ErrorCode::Unauthorized, "admin token not configured"))?; 21 + 22 + let auth_value = headers 23 + .get(axum::http::header::AUTHORIZATION) 24 + .and_then(|v| { 25 + v.to_str() 26 + .inspect_err(|_| { 27 + tracing::debug!( 28 + "Authorization header contains non-UTF-8 bytes; treating as absent" 29 + ); 30 + }) 31 + .ok() 32 + }) 33 + .unwrap_or(""); 34 + 35 + let provided_token = auth_value.strip_prefix("Bearer ").ok_or_else(|| { 36 + ApiError::new( 37 + ErrorCode::Unauthorized, 38 + "missing or invalid Authorization header", 39 + ) 40 + })?; 41 + 42 + if provided_token 43 + .as_bytes() 44 + .ct_eq(expected_token.as_bytes()) 45 + .unwrap_u8() 46 + != 1 47 + { 48 + return Err(ApiError::new(ErrorCode::Unauthorized, "invalid admin token")); 49 + } 50 + 51 + Ok(()) 52 + }
+6 -64
crates/relay/src/routes/claim_codes.rs
··· 1 1 // pattern: Imperative Shell 2 2 // 3 - // Gathers: Bearer token from Authorization header, JSON request body, config, DB pool 3 + // Gathers: admin Bearer token (Authorization header), JSON request body, DB pool 4 4 // Processes: auth check → input validation → code generation → DB batch insert (transaction) 5 5 // Returns: JSON { codes: [...] } on success; ApiError on all failure paths 6 6 7 7 use axum::{extract::State, http::HeaderMap, response::Json}; 8 - use rand_core::{OsRng, RngCore}; 9 8 use serde::{Deserialize, Serialize}; 10 - use subtle::ConstantTimeEq; 11 9 12 10 use common::{ApiError, ErrorCode}; 13 11 14 12 use crate::app::AppState; 13 + use crate::routes::auth::require_admin_token; 14 + use crate::routes::code_gen::generate_code; 15 15 16 16 const MAX_COUNT: u32 = 10; 17 - const CODE_LEN: usize = 6; 18 - const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; 19 17 20 18 fn default_expires_in_hours() -> u32 { 21 19 24 ··· 42 40 ) -> Result<Json<ClaimCodesResponse>, ApiError> { 43 41 // --- Auth: require matching Bearer token --- 44 42 // Check this first so unauthenticated callers cannot probe server configuration. 45 - let expected_token = state 46 - .config 47 - .admin_token 48 - .as_deref() 49 - .ok_or_else(|| ApiError::new(ErrorCode::Unauthorized, "admin token not configured"))?; 50 - 51 - let auth_value = headers 52 - .get(axum::http::header::AUTHORIZATION) 53 - .and_then(|v| { 54 - v.to_str() 55 - .inspect_err(|_| { 56 - tracing::debug!( 57 - "Authorization header contains non-UTF-8 bytes; treating as absent" 58 - ); 59 - }) 60 - .ok() 61 - }) 62 - .unwrap_or(""); 63 - 64 - let provided_token = auth_value.strip_prefix("Bearer ").ok_or_else(|| { 65 - ApiError::new( 66 - ErrorCode::Unauthorized, 67 - "missing or invalid Authorization header", 68 - ) 69 - })?; 70 - 71 - if provided_token 72 - .as_bytes() 73 - .ct_eq(expected_token.as_bytes()) 74 - .unwrap_u8() 75 - != 1 76 - { 77 - return Err(ApiError::new( 78 - ErrorCode::Unauthorized, 79 - "invalid admin token", 80 - )); 81 - } 43 + require_admin_token(&headers, &state)?; 82 44 83 45 // --- Validate input --- 84 46 if payload.count == 0 || payload.count > MAX_COUNT { ··· 130 92 codes.into_iter().collect() 131 93 } 132 94 133 - /// Generate a single 6-character uppercase alphanumeric code. 134 - fn generate_code() -> String { 135 - let mut buf = [0u8; CODE_LEN]; 136 - OsRng.fill_bytes(&mut buf); 137 - buf.iter() 138 - .map(|&b| CHARSET[(b as usize) % CHARSET.len()] as char) 139 - .collect() 140 - } 141 - 142 95 /// Insert all codes in a single transaction; returns Err if any INSERT fails. 143 96 async fn insert_claim_codes( 144 97 db: &sqlx::SqlitePool, ··· 175 128 176 129 #[cfg(test)] 177 130 mod tests { 178 - use std::sync::Arc; 179 - 180 131 use axum::{ 181 132 body::Body, 182 133 http::{Request, StatusCode}, 183 134 }; 184 135 use tower::ServiceExt; 185 136 186 - use crate::app::{app, test_state, AppState}; 137 + use crate::app::{app, test_state}; 138 + use crate::routes::test_utils::test_state_with_admin_token; 187 139 188 140 // ── Helpers ────────────────────────────────────────────────────────────── 189 - 190 - async fn test_state_with_admin_token() -> AppState { 191 - let base = test_state().await; 192 - let mut config = (*base.config).clone(); 193 - config.admin_token = Some("test-admin-token".to_string()); 194 - AppState { 195 - config: Arc::new(config), 196 - db: base.db, 197 - } 198 - } 199 141 200 142 fn post_claim_codes(body: &str, bearer: Option<&str>) -> Request<Body> { 201 143 let mut builder = Request::builder()
+13
crates/relay/src/routes/code_gen.rs
··· 1 + use rand_core::{OsRng, RngCore}; 2 + 3 + pub const CODE_LEN: usize = 6; 4 + pub const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; 5 + 6 + /// Generate a single 6-character uppercase alphanumeric code. 7 + pub fn generate_code() -> String { 8 + let mut buf = [0u8; CODE_LEN]; 9 + OsRng.fill_bytes(&mut buf); 10 + buf.iter() 11 + .map(|&b| CHARSET[(b as usize) % CHARSET.len()] as char) 12 + .collect() 13 + }
+23 -92
crates/relay/src/routes/create_account.rs
··· 1 1 // pattern: Imperative Shell 2 2 // 3 - // Gathers: Bearer token from Authorization header, JSON request body, config, DB pool 3 + // Gathers: admin Bearer token (Authorization header), JSON request body, config, DB pool 4 4 // Processes: auth check → handle validation → tier validation → email uniqueness → 5 5 // handle uniqueness → account_id generation → claim code generation → 6 6 // DB transaction (claim_codes + pending_accounts insert) ··· 11 11 http::{HeaderMap, StatusCode}, 12 12 response::Json, 13 13 }; 14 - use rand_core::{OsRng, RngCore}; 15 14 use serde::{Deserialize, Serialize}; 16 - use subtle::ConstantTimeEq; 17 15 use uuid::Uuid; 18 16 19 17 use common::{ApiError, ErrorCode}; 20 18 21 19 use crate::app::AppState; 20 + use crate::routes::auth::require_admin_token; 21 + use crate::routes::code_gen::generate_code; 22 22 23 - const CODE_LEN: usize = 6; 24 - const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; 25 23 const CLAIM_CODE_EXPIRES_IN_HOURS: u32 = 24; 26 24 27 25 #[derive(Deserialize)] ··· 47 45 Json(payload): Json<CreateAccountRequest>, 48 46 ) -> Result<(StatusCode, Json<CreateAccountResponse>), ApiError> { 49 47 // --- Auth: require matching Bearer token --- 50 - let expected_token = state 51 - .config 52 - .admin_token 53 - .as_deref() 54 - .ok_or_else(|| ApiError::new(ErrorCode::Unauthorized, "admin token not configured"))?; 55 - 56 - let auth_value = headers 57 - .get(axum::http::header::AUTHORIZATION) 58 - .and_then(|v| { 59 - v.to_str() 60 - .inspect_err(|_| { 61 - tracing::debug!( 62 - "Authorization header contains non-UTF-8 bytes; treating as absent" 63 - ); 64 - }) 65 - .ok() 66 - }) 67 - .unwrap_or(""); 68 - 69 - let provided_token = auth_value.strip_prefix("Bearer ").ok_or_else(|| { 70 - ApiError::new( 71 - ErrorCode::Unauthorized, 72 - "missing or invalid Authorization header", 73 - ) 74 - })?; 75 - 76 - if provided_token 77 - .as_bytes() 78 - .ct_eq(expected_token.as_bytes()) 79 - .unwrap_u8() 80 - != 1 81 - { 82 - return Err(ApiError::new(ErrorCode::Unauthorized, "invalid admin token")); 83 - } 48 + require_admin_token(&headers, &state)?; 84 49 85 50 // --- Validate handle format --- 86 51 if let Err(msg) = validate_handle(&payload.handle) { ··· 95 60 )); 96 61 } 97 62 98 - // --- Email uniqueness: check accounts and pending_accounts --- 99 - let email_in_accounts: bool = sqlx::query_scalar( 100 - "SELECT EXISTS(SELECT 1 FROM accounts WHERE email = ?)", 63 + // --- Email uniqueness: check accounts and pending_accounts in one query --- 64 + // Fast-path optimization: reject before the INSERT to avoid touching the claim code retry 65 + // loop on a predictable failure. The unique indexes on pending_accounts.email and 66 + // accounts.email are the authoritative enforcement; this is an early return. 67 + // Note: pending_accounts has no cross-table FK to accounts, so both tables must be checked. 68 + let email_taken: bool = sqlx::query_scalar( 69 + "SELECT EXISTS(SELECT 1 FROM accounts WHERE email = ?) 70 + OR EXISTS(SELECT 1 FROM pending_accounts WHERE email = ?)", 101 71 ) 102 72 .bind(&payload.email) 103 - .fetch_one(&state.db) 104 - .await 105 - .map_err(|e| { 106 - tracing::error!(error = %e, "failed to check email uniqueness in accounts"); 107 - ApiError::new(ErrorCode::InternalError, "failed to create account") 108 - })?; 109 - 110 - let email_in_pending: bool = sqlx::query_scalar( 111 - "SELECT EXISTS(SELECT 1 FROM pending_accounts WHERE email = ?)", 112 - ) 113 73 .bind(&payload.email) 114 74 .fetch_one(&state.db) 115 75 .await 116 76 .map_err(|e| { 117 - tracing::error!(error = %e, "failed to check email uniqueness in pending_accounts"); 77 + tracing::error!(error = %e, "failed to check email uniqueness"); 118 78 ApiError::new(ErrorCode::InternalError, "failed to create account") 119 79 })?; 120 80 121 - if email_in_accounts || email_in_pending { 81 + if email_taken { 122 82 return Err(ApiError::new( 123 83 ErrorCode::AccountExists, 124 84 "an account with this email already exists", 125 85 )); 126 86 } 127 87 128 - // --- Handle uniqueness: check handles and pending_accounts --- 129 - let handle_in_handles: bool = sqlx::query_scalar( 130 - "SELECT EXISTS(SELECT 1 FROM handles WHERE handle = ?)", 88 + // --- Handle uniqueness: check handles and pending_accounts in one query --- 89 + let handle_taken: bool = sqlx::query_scalar( 90 + "SELECT EXISTS(SELECT 1 FROM handles WHERE handle = ?) 91 + OR EXISTS(SELECT 1 FROM pending_accounts WHERE handle = ?)", 131 92 ) 132 93 .bind(&payload.handle) 133 - .fetch_one(&state.db) 134 - .await 135 - .map_err(|e| { 136 - tracing::error!(error = %e, "failed to check handle uniqueness in handles"); 137 - ApiError::new(ErrorCode::InternalError, "failed to create account") 138 - })?; 139 - 140 - let handle_in_pending: bool = sqlx::query_scalar( 141 - "SELECT EXISTS(SELECT 1 FROM pending_accounts WHERE handle = ?)", 142 - ) 143 94 .bind(&payload.handle) 144 95 .fetch_one(&state.db) 145 96 .await 146 97 .map_err(|e| { 147 - tracing::error!(error = %e, "failed to check handle uniqueness in pending_accounts"); 98 + tracing::error!(error = %e, "failed to check handle uniqueness"); 148 99 ApiError::new(ErrorCode::InternalError, "failed to create account") 149 100 })?; 150 101 151 - if handle_in_handles || handle_in_pending { 102 + if handle_taken { 152 103 return Err(ApiError::new( 153 104 ErrorCode::HandleTaken, 154 105 "this handle is already claimed", ··· 240 191 matches!(tier, "free" | "pro" | "business") 241 192 } 242 193 243 - /// Generate a single 6-character uppercase alphanumeric claim code. 244 - fn generate_code() -> String { 245 - let mut buf = [0u8; CODE_LEN]; 246 - OsRng.fill_bytes(&mut buf); 247 - buf.iter() 248 - .map(|&b| CHARSET[(b as usize) % CHARSET.len()] as char) 249 - .collect() 250 - } 251 - 252 194 /// Insert a claim code and its associated pending account in a single transaction. 253 195 async fn insert_pending_account( 254 196 db: &sqlx::SqlitePool, ··· 330 272 331 273 #[cfg(test)] 332 274 mod tests { 333 - use std::sync::Arc; 334 - 335 275 use axum::{ 336 276 body::Body, 337 277 http::{Request, StatusCode}, 338 278 }; 339 279 use tower::ServiceExt; 340 280 341 - use crate::app::{app, test_state, AppState}; 281 + use crate::app::{app, test_state}; 282 + use crate::routes::test_utils::test_state_with_admin_token; 342 283 343 284 // ── Helpers ─────────────────────────────────────────────────────────────── 344 - 345 - async fn test_state_with_admin_token() -> AppState { 346 - let base = test_state().await; 347 - let mut config = (*base.config).clone(); 348 - config.admin_token = Some("test-admin-token".to_string()); 349 - AppState { 350 - config: Arc::new(config), 351 - db: base.db, 352 - } 353 - } 354 285 355 286 fn post_create_account(body: &str, bearer: Option<&str>) -> Request<Body> { 356 287 let mut builder = Request::builder() ··· 549 480 550 481 #[tokio::test] 551 482 async fn duplicate_handle_in_handles_returns_409() { 552 - // handle_in_handles query (line ~129) coverage 483 + // handle_in_handles query coverage 553 484 let state = test_state_with_admin_token().await; 554 485 555 486 // Seed a fully-provisioned account with an active handle.
+3 -31
crates/relay/src/routes/create_signing_key.rs
··· 1 1 // pattern: Imperative Shell 2 2 // 3 - // Gathers: Bearer token from Authorization header, JSON request body, config, DB pool 3 + // Gathers: admin Bearer token (Authorization header), JSON request body, config, DB pool 4 4 // Processes: auth check → algorithm check → master key check → key generation → encryption → DB insert 5 5 // Returns: JSON { key_id, public_key, algorithm } on success; ApiError on all failure paths 6 6 7 7 use axum::{extract::State, http::HeaderMap, response::Json}; 8 8 use serde::{Deserialize, Serialize}; 9 - use subtle::ConstantTimeEq; 10 9 11 10 use common::{ApiError, ErrorCode}; 12 11 13 12 use crate::app::AppState; 13 + use crate::routes::auth::require_admin_token; 14 14 15 15 #[derive(Deserialize)] 16 16 #[serde(rename_all = "lowercase")] ··· 43 43 ) -> Result<Json<CreateSigningKeyResponse>, ApiError> { 44 44 // --- Auth: require matching Bearer token --- 45 45 // Check this first so unauthenticated callers cannot probe server configuration. 46 - let expected_token = state 47 - .config 48 - .admin_token 49 - .as_deref() 50 - .ok_or_else(|| ApiError::new(ErrorCode::Unauthorized, "admin token not configured"))?; 51 - 52 - let auth_value = headers 53 - .get(axum::http::header::AUTHORIZATION) 54 - .and_then(|v| v.to_str().ok()) 55 - .unwrap_or(""); 56 - 57 - let provided_token = auth_value.strip_prefix("Bearer ").ok_or_else(|| { 58 - ApiError::new( 59 - ErrorCode::Unauthorized, 60 - "missing or invalid Authorization header", 61 - ) 62 - })?; 63 - 64 - if provided_token 65 - .as_bytes() 66 - .ct_eq(expected_token.as_bytes()) 67 - .unwrap_u8() 68 - != 1 69 - { 70 - return Err(ApiError::new( 71 - ErrorCode::Unauthorized, 72 - "invalid admin token", 73 - )); 74 - } 46 + require_admin_token(&headers, &state)?; 75 47 76 48 // --- Master key: return 503 if not configured --- 77 49 let master_key: &[u8; 32] = state
+6
crates/relay/src/routes/mod.rs
··· 1 + pub mod auth; 1 2 pub mod claim_codes; 2 3 pub mod create_account; 3 4 pub mod create_signing_key; 4 5 pub mod describe_server; 5 6 pub mod health; 7 + 8 + mod code_gen; 9 + 10 + #[cfg(test)] 11 + pub(crate) mod test_utils;
+18
crates/relay/src/routes/test_utils.rs
··· 1 + use std::sync::Arc; 2 + 3 + use crate::app::{test_state, AppState}; 4 + 5 + /// Minimal test state with admin_token set to `"test-admin-token"`. 6 + /// 7 + /// Wraps `test_state()` and overrides the single config field that most 8 + /// admin-endpoint tests need. Defined once here to avoid copying the same 9 + /// 8-line block into every route test module. 10 + pub async fn test_state_with_admin_token() -> AppState { 11 + let base = test_state().await; 12 + let mut config = (*base.config).clone(); 13 + config.admin_token = Some("test-admin-token".to_string()); 14 + AppState { 15 + config: Arc::new(config), 16 + db: base.db, 17 + } 18 + }