From b73c91b561fe86a57c0f68ffe3e253d56c93608d Mon Sep 17 00:00:00 2001 From: lewis Date: Fri, 30 Jan 2026 20:53:31 +0100 Subject: [PATCH] fix: account creation bug & frontend style improvements --- crates/tranquil-db/src/postgres/user.rs | 23 +- crates/tranquil-pds/src/api/proxy.rs | 46 +- .../src/auth/verification_token.rs | 18 +- .../src/oauth/endpoints/authorize.rs | 8 +- .../tests/account_notifications.rs | 4 +- crates/tranquil-pds/tests/common/mod.rs | 14 +- crates/tranquil-pds/tests/email_update.rs | 11 +- crates/tranquil-pds/tests/jwt_security.rs | 6 +- frontend/public/homepage.html | 385 +++++------- frontend/src/components/RandomHandle.svelte | 21 + frontend/src/components/ReauthModal.svelte | 5 - frontend/src/lib/api.ts | 12 +- frontend/src/lib/auth.svelte.ts | 2 +- frontend/src/lib/authenticated-client.ts | 8 +- frontend/src/lib/migration/atproto-client.ts | 4 +- frontend/src/lib/registration/flow.svelte.ts | 40 +- frontend/src/lib/types/totp-state.ts | 17 +- frontend/src/locales/en.json | 418 ++++++------- frontend/src/locales/fi.json | 234 +++---- frontend/src/locales/ja.json | 231 +++---- frontend/src/locales/ko.json | 227 +++---- frontend/src/locales/sv.json | 231 +++---- frontend/src/locales/zh.json | 223 +++---- frontend/src/routes/Login.svelte | 126 ++-- frontend/src/routes/OAuthAccounts.svelte | 8 +- frontend/src/routes/OAuthLogin.svelte | 21 +- frontend/src/routes/OAuthPasskey.svelte | 10 +- frontend/src/routes/OAuthTotp.svelte | 24 +- frontend/src/routes/Register.svelte | 348 ++++++----- frontend/src/routes/RegisterPassword.svelte | 578 ++++++++++-------- frontend/src/routes/RegisterSso.svelte | 57 +- frontend/src/routes/Verify.svelte | 21 +- 32 files changed, 1631 insertions(+), 1750 deletions(-) create mode 100644 frontend/src/components/RandomHandle.svelte diff --git a/crates/tranquil-db/src/postgres/user.rs b/crates/tranquil-db/src/postgres/user.rs index df63f75..ca23487 100644 --- a/crates/tranquil-db/src/postgres/user.rs +++ b/crates/tranquil-db/src/postgres/user.rs @@ -2334,7 +2334,12 @@ impl UserRepository for PostgresUserRepository { tranquil_db_traits::CreatePasswordAccountResult, tranquil_db_traits::CreateAccountError, > { + tracing::info!(did = %input.did, handle = %input.handle, "create_password_account: starting transaction"); let mut tx = self.pool.begin().await.map_err(|e: sqlx::Error| { + tracing::error!( + "create_password_account: failed to begin transaction: {}", + e + ); tranquil_db_traits::CreateAccountError::Database(e.to_string()) })?; @@ -2366,8 +2371,12 @@ impl UserRepository for PostgresUserRepository { .await; let user_id = match user_insert { - Ok((id,)) => id, + Ok((id,)) => { + tracing::info!(did = %input.did, user_id = %id, "create_password_account: user row inserted"); + id + } Err(e) => { + tracing::error!(did = %input.did, error = %e, "create_password_account: user insert failed"); if let Some(db_err) = e.as_database_error() && db_err.code().as_deref() == Some("23505") { @@ -2455,8 +2464,7 @@ impl UserRepository for PostgresUserRepository { if let Some(birthdate_pref) = &input.birthdate_pref { let _ = sqlx::query!( - "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3) - ON CONFLICT (user_id, name) DO NOTHING", + "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)", user_id, "app.bsky.actor.defs#personalDetailsPref", birthdate_pref @@ -2465,9 +2473,12 @@ impl UserRepository for PostgresUserRepository { .await; } + tracing::info!(did = %input.did, user_id = %user_id, "create_password_account: committing transaction"); tx.commit().await.map_err(|e: sqlx::Error| { + tracing::error!(did = %input.did, user_id = %user_id, error = %e, "create_password_account: commit failed"); tranquil_db_traits::CreateAccountError::Database(e.to_string()) })?; + tracing::info!(did = %input.did, user_id = %user_id, "create_password_account: transaction committed successfully"); Ok(tranquil_db_traits::CreatePasswordAccountResult { user_id, @@ -2716,8 +2727,7 @@ impl UserRepository for PostgresUserRepository { if let Some(birthdate_pref) = &input.birthdate_pref { let _ = sqlx::query!( - "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3) - ON CONFLICT (user_id, name) DO NOTHING", + "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)", user_id, "app.bsky.actor.defs#personalDetailsPref", birthdate_pref @@ -2865,8 +2875,7 @@ impl UserRepository for PostgresUserRepository { if let Some(birthdate_pref) = &input.birthdate_pref { let _ = sqlx::query!( - "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3) - ON CONFLICT (user_id, name) DO NOTHING", + "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)", user_id, "app.bsky.actor.defs#personalDetailsPref", birthdate_pref diff --git a/crates/tranquil-pds/src/api/proxy.rs b/crates/tranquil-pds/src/api/proxy.rs index 7488ea0..a5021f9 100644 --- a/crates/tranquil-pds/src/api/proxy.rs +++ b/crates/tranquil-pds/src/api/proxy.rs @@ -245,36 +245,34 @@ async fn proxy_handler( let key_bytes = match auth_user.key_bytes { Some(kb) => kb, - None => { - match state.user_repo.get_user_info_by_did(&auth_user.did).await { - Ok(Some(info)) => match info.key_bytes { - Some(key_bytes_enc) => { - match crate::config::decrypt_key( - &key_bytes_enc, - info.encryption_version, - ) { - Ok(key) => key, - Err(e) => { - error!(error = ?e, "Failed to decrypt user key for proxy"); - return ApiError::UpstreamFailure.into_response(); - } + None => match state.user_repo.get_user_info_by_did(&auth_user.did).await { + Ok(Some(info)) => match info.key_bytes { + Some(key_bytes_enc) => { + match crate::config::decrypt_key( + &key_bytes_enc, + info.encryption_version, + ) { + Ok(key) => key, + Err(e) => { + error!(error = ?e, "Failed to decrypt user key for proxy"); + return ApiError::UpstreamFailure.into_response(); } } - None => { - warn!(did = %auth_user.did, "User has no signing key for proxy"); - return ApiError::UpstreamFailure.into_response(); - } - }, - Ok(None) => { - warn!(did = %auth_user.did, "User not found for proxy service auth"); - return ApiError::UpstreamFailure.into_response(); } - Err(e) => { - error!(error = ?e, "DB error fetching user key for proxy"); + None => { + warn!(did = %auth_user.did, "User has no signing key for proxy"); return ApiError::UpstreamFailure.into_response(); } + }, + Ok(None) => { + warn!(did = %auth_user.did, "User not found for proxy service auth"); + return ApiError::UpstreamFailure.into_response(); } - } + Err(e) => { + error!(error = ?e, "DB error fetching user key for proxy"); + return ApiError::UpstreamFailure.into_response(); + } + }, }; match crate::auth::create_service_token( diff --git a/crates/tranquil-pds/src/auth/verification_token.rs b/crates/tranquil-pds/src/auth/verification_token.rs index 3d62aef..08fd2db 100644 --- a/crates/tranquil-pds/src/auth/verification_token.rs +++ b/crates/tranquil-pds/src/auth/verification_token.rs @@ -296,21 +296,11 @@ pub fn verify_token_signature(token: &str) -> Result String { - token - .replace(['-', ' '], "") - .chars() - .collect::>() - .chunks(4) - .map(|chunk| chunk.iter().collect::()) - .collect::>() - .join("-") + token.to_string() } pub fn normalize_token_input(input: &str) -> String { - input - .chars() - .filter(|c| c.is_ascii_alphanumeric() || *c == '_' || *c == '=') - .collect() + input.trim().to_string() } #[cfg(test)] @@ -410,12 +400,12 @@ mod tests { fn test_format_token_for_display() { let token = "ABCDEFGHIJKLMNOP"; let formatted = format_token_for_display(token); - assert_eq!(formatted, "ABCD-EFGH-IJKL-MNOP"); + assert_eq!(formatted, "ABCDEFGHIJKLMNOP"); } #[test] fn test_normalize_token_input() { - let input = "ABCD-EFGH IJKL-MNOP"; + let input = " ABCDEFGHIJKLMNOP "; let normalized = normalize_token_input(input); assert_eq!(normalized, "ABCDEFGHIJKLMNOP"); } diff --git a/crates/tranquil-pds/src/oauth/endpoints/authorize.rs b/crates/tranquil-pds/src/oauth/endpoints/authorize.rs index 373514a..c764ead 100644 --- a/crates/tranquil-pds/src/oauth/endpoints/authorize.rs +++ b/crates/tranquil-pds/src/oauth/endpoints/authorize.rs @@ -3277,10 +3277,16 @@ pub async fn register_complete( } }; - let password_valid = password_hashes.iter().fold(false, |acc, hash| { + let mut password_valid = password_hashes.iter().fold(false, |acc, hash| { acc | bcrypt::verify(&form.app_password, hash).unwrap_or(false) }); + if !password_valid + && let Ok(Some(account_hash)) = state.user_repo.get_password_hash_by_did(&did).await + { + password_valid = bcrypt::verify(&form.app_password, &account_hash).unwrap_or(false); + } + if !password_valid { return ( StatusCode::FORBIDDEN, diff --git a/crates/tranquil-pds/tests/account_notifications.rs b/crates/tranquil-pds/tests/account_notifications.rs index d31807b..19c5b09 100644 --- a/crates/tranquil-pds/tests/account_notifications.rs +++ b/crates/tranquil-pds/tests/account_notifications.rs @@ -227,13 +227,13 @@ async fn test_update_email_via_notification_prefs() { .skip_while(|line| !line.contains("verification code")) .nth(1) .map(|line| line.trim().to_string()) - .filter(|line| !line.is_empty() && line.contains('-')) + .filter(|line| !line.is_empty()) .unwrap_or_else(|| { body_text .lines() .find(|line| { let trimmed = line.trim(); - trimmed.starts_with("MX") && trimmed.contains('-') + trimmed.len() == 11 && trimmed.chars().nth(5) == Some('-') }) .map(|s| s.trim().to_string()) .unwrap_or_default() diff --git a/crates/tranquil-pds/tests/common/mod.rs b/crates/tranquil-pds/tests/common/mod.rs index 36397d1..45eeadd 100644 --- a/crates/tranquil-pds/tests/common/mod.rs +++ b/crates/tranquil-pds/tests/common/mod.rs @@ -605,9 +605,9 @@ pub async fn verify_new_account(client: &Client, did: &str) -> String { .and_then(|(i, _)| lines.get(i + 1).map(|s| s.trim().to_string())) .or_else(|| { body_text - .split_whitespace() - .find(|word| word.contains('-') && word.chars().filter(|c| *c == '-').count() >= 3) - .map(|s| s.to_string()) + .lines() + .find(|line| line.trim().starts_with("MX")) + .map(|s| s.trim().to_string()) }) .unwrap_or_else(|| body_text.clone()); @@ -774,11 +774,9 @@ async fn create_account_and_login_internal(client: &Client, make_admin: bool) -> .and_then(|(i, _)| lines.get(i + 1).map(|s: &&str| s.trim().to_string())) .or_else(|| { body_text - .split_whitespace() - .find(|word: &&str| { - word.contains('-') && word.chars().filter(|c| *c == '-').count() >= 3 - }) - .map(|s: &str| s.to_string()) + .lines() + .find(|line| line.trim().starts_with("MX")) + .map(|s| s.trim().to_string()) }) .unwrap_or_else(|| body_text.clone()); diff --git a/crates/tranquil-pds/tests/email_update.rs b/crates/tranquil-pds/tests/email_update.rs index 51fb8a1..ee628dd 100644 --- a/crates/tranquil-pds/tests/email_update.rs +++ b/crates/tranquil-pds/tests/email_update.rs @@ -17,11 +17,14 @@ async fn get_email_update_token(pool: &PgPool, did: &str) -> String { .skip_while(|line| !line.contains("verification code")) .nth(1) .map(|line| line.trim().to_string()) - .filter(|line| !line.is_empty() && line.contains('-')) + .filter(|line| !line.is_empty()) .unwrap_or_else(|| { body_text .lines() - .find(|line| line.trim().starts_with("MX") && line.contains('-')) + .find(|line| { + let trimmed = line.trim(); + trimmed.len() == 11 && trimmed.chars().nth(5) == Some('-') + }) .map(|s| s.trim().to_string()) .unwrap_or_default() }) @@ -271,7 +274,7 @@ async fn test_confirm_email_confirms_existing_email() { let code = body_text .lines() - .find(|line| line.trim().starts_with("MX") && line.contains('-')) + .find(|line| line.trim().starts_with("MX")) .map(|s| s.trim().to_string()) .unwrap_or_default(); @@ -334,7 +337,7 @@ async fn test_confirm_email_rejects_wrong_email() { let code = body_text .lines() - .find(|line| line.trim().starts_with("MX") && line.contains('-')) + .find(|line| line.trim().starts_with("MX")) .map(|s| s.trim().to_string()) .unwrap_or_default(); diff --git a/crates/tranquil-pds/tests/jwt_security.rs b/crates/tranquil-pds/tests/jwt_security.rs index 1071a45..7a97348 100644 --- a/crates/tranquil-pds/tests/jwt_security.rs +++ b/crates/tranquil-pds/tests/jwt_security.rs @@ -696,9 +696,9 @@ async fn test_refresh_token_replay_protection() { .and_then(|(i, _)| lines.get(i + 1).map(|s| s.trim().to_string())) .or_else(|| { body_text - .split_whitespace() - .find(|word| word.contains('-') && word.chars().filter(|c| *c == '-').count() >= 3) - .map(|s| s.to_string()) + .lines() + .find(|line| line.trim().starts_with("MX")) + .map(|s| s.trim().to_string()) }) .unwrap_or_else(|| body_text.clone()); diff --git a/frontend/public/homepage.html b/frontend/public/homepage.html index 8c15f5e..7348c50 100644 --- a/frontend/public/homepage.html +++ b/frontend/public/homepage.html @@ -129,36 +129,14 @@ -webkit-font-smoothing: antialiased; } - .pattern-container { + .pattern-canvas { position: fixed; - top: -32px; - left: -32px; - right: -32px; - bottom: -32px; - pointer-events: none; - z-index: 1; - overflow: hidden; - } - .pattern { - position: absolute; top: 0; left: 0; - width: calc(100% + 500px); + width: 100%; height: 100%; - animation: drift 80s linear infinite; - } - .dot { - position: absolute; - width: 10px; - height: 10px; - background: rgba(0, 0, 0, 0.06); - border-radius: 50%; - transition: transform 0.04s linear; - } - @media (prefers-color-scheme: dark) { - .dot { - background: rgba(255, 255, 255, 0.1); - } + pointer-events: none; + z-index: 1; } .pattern-fade { position: fixed; @@ -174,14 +152,6 @@ pointer-events: none; z-index: 2; } - @keyframes drift { - 0% { - transform: translateX(-500px); - } - 100% { - transform: translateX(0); - } - } nav { position: fixed; @@ -246,9 +216,8 @@ padding: 72px 32px 32px; } .hero { - padding: var(--space-7) 0 var(--space-8); - border-bottom: 1px solid var(--border-color); - margin-bottom: var(--space-8); + padding: var(--space-7) 0 var(--space-6); + margin-bottom: var(--space-5); } h1 { font-size: var(--text-4xl); @@ -294,16 +263,16 @@ border: 1px solid transparent; } .btn.primary { - background: var(--secondary); + background: var(--accent); color: var(--text-inverse); - border-color: var(--secondary); + border-color: var(--accent); } .btn.primary:hover { - background: var(--secondary-hover); - border-color: var(--secondary-hover); + background: var(--accent-hover); + border-color: var(--accent-hover); } .btn.secondary { - background: transparent; + background: var(--bg-primary); color: var(--text-primary); border-color: var(--border-color); } @@ -349,34 +318,20 @@ margin-bottom: var(--space-5); line-height: var(--leading-relaxed); } - .features { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: var(--space-6); - margin: var(--space-6) 0 var(--space-8); - } .feature { - padding: var(--space-5); + padding: var(--space-6); background: var(--bg-secondary); border-radius: var(--radius-xl); border: 1px solid var(--border-color); - } - .feature h3 { - font-size: var(--text-base); - font-weight: var(--font-semibold); - color: var(--text-primary); - margin-bottom: var(--space-3); + margin: var(--space-6) 0 var(--space-8); } .feature p { - font-size: var(--text-sm); + font-size: var(--text-base); color: var(--text-secondary); margin: 0; line-height: var(--leading-relaxed); } @media (max-width: 700px) { - .features { - grid-template-columns: 1fr; - } h1 { font-size: var(--text-3xl); } @@ -407,9 +362,7 @@ -
-
-
+