diff --git a/src/crates/services/miniapp-market-service/README.md b/src/crates/services/miniapp-market-service/README.md index 55470cc63f..19e363a2a8 100644 --- a/src/crates/services/miniapp-market-service/README.md +++ b/src/crates/services/miniapp-market-service/README.md @@ -193,6 +193,9 @@ then run `cargo test -p openbitfun-miniapp-market-service --lib email_auth::test and a real message render check after SMTP/template changes. Marketplace author labels are public: email accounts display their full verified -email address in `user.login`, listing owners, and moderation submitters. GitHub +email address in listing owners and moderation submitters. The authenticated +`/me` profile keeps its legacy `user.login` protocol handle and exposes the +verified address separately as `email`; older relays require that handle format. +Skin uses the separate email for its public author projection. GitHub accounts retain their GitHub login. Ownership and authorization use internal IDs, never the displayed label; this does not link email and GitHub accounts. diff --git a/src/crates/services/miniapp-market-service/src/db.rs b/src/crates/services/miniapp-market-service/src/db.rs index 4bf2ce8dc9..3708ee2d6b 100644 --- a/src/crates/services/miniapp-market-service/src/db.rs +++ b/src/crates/services/miniapp-market-service/src/db.rs @@ -312,9 +312,9 @@ fn user_from_row(row: sqlx::sqlite::SqliteRow) -> AuthenticatedUser { None => format!("email-{}", row.get::("id")), }), github_id: row.get::, _>("github_id").unwrap_or_default(), - login: row - .get::, _>("email") - .unwrap_or_else(|| row.get("login")), + // Existing relay versions validate this protocol handle as a GitHub-style + // username. The verified email is carried separately for display. + login: row.get("login"), avatar_url: row.get("avatar_url"), }, } diff --git a/src/crates/services/miniapp-market-service/src/email_auth.rs b/src/crates/services/miniapp-market-service/src/email_auth.rs index 2758eb828b..f4ed37137f 100644 --- a/src/crates/services/miniapp-market-service/src/email_auth.rs +++ b/src/crates/services/miniapp-market-service/src/email_auth.rs @@ -568,7 +568,13 @@ mod tests { assert_eq!(user.profile.github_id, 0); assert_eq!(user.email.as_deref(), Some("alice@example.com")); assert_eq!(github.email, None); - assert_eq!(user.profile.login, "alice@example.com"); + // Keep the legacy relay profile contract while exposing email separately. + assert!(user.profile.login.len() <= 100); + assert!(user + .profile + .login + .bytes() + .all(|c| c.is_ascii_alphanumeric() || c == b'-')); assert!(user.profile.identity_id().unwrap().starts_with("email-")); assert!(!service.is_admin(&user)); let poll = service @@ -1009,7 +1015,7 @@ mod tests { serde_json::from_slice(&to_bytes(response.into_body(), 16384).await.unwrap()).unwrap(); assert_eq!(profile["email"], "alice@example.com"); assert!(profile["user"].get("email").is_none()); - assert_eq!(profile["user"]["login"], "alice@example.com"); + assert!(!profile["user"]["login"].as_str().unwrap().contains('@')); } #[test] diff --git a/src/crates/services/skin-market-service/src/auth.rs b/src/crates/services/skin-market-service/src/auth.rs index 3597090b17..0c8bb2af77 100644 --- a/src/crates/services/skin-market-service/src/auth.rs +++ b/src/crates/services/skin-market-service/src/auth.rs @@ -26,6 +26,8 @@ pub(crate) struct AuthenticatedIdentity { #[serde(rename_all = "camelCase")] struct IdentityResponse { user: AppearanceMarketUserSummary, + #[serde(default)] + email: Option, is_admin: bool, } @@ -117,9 +119,21 @@ impl IdentityVerifier { "The identity service rejected the verification request.", )); } - let identity: IdentityResponse = response.json().await.map_err(|_| { + let mut identity: IdentityResponse = response.json().await.map_err(|_| { SkinMarketError::unavailable("The identity service returned an invalid response.") })?; + // Preserve the authority's protocol handle for older relays; only this + // marketplace's public author projection uses the verified email. + if identity.user.github_id == 0 { + if let Some(email) = identity.email.filter(|email| !email.is_empty()) { + if email.len() > 254 || email.chars().any(char::is_control) { + return Err(SkinMarketError::unavailable( + "The identity service returned an invalid email.", + )); + } + identity.user.login = email; + } + } if identity.user.identity_id().is_none() || identity.user.login.trim().is_empty() || identity.user.login.len() > 254 diff --git a/src/crates/services/skin-market-service/src/lib.rs b/src/crates/services/skin-market-service/src/lib.rs index a5a3534ff6..e8ff1d81c4 100644 --- a/src/crates/services/skin-market-service/src/lib.rs +++ b/src/crates/services/skin-market-service/src/lib.rs @@ -148,7 +148,7 @@ mod tests { .unwrap_or_default(); let (github_id, login, is_admin) = match token { "Bearer owner-token" => (41, "owner", false), - "Bearer email-token" => (0, "author@example.com", false), + "Bearer email-token" => (0, "user-legacy-handle", false), "Bearer admin-token" => (42, "admin", true), _ => { return ( @@ -166,6 +166,7 @@ mod tests { "login": login, "avatarUrl": "https://example.invalid/avatar" }, + "email": if github_id == 0 { Some("author@example.com") } else { None }, "isAdmin": is_admin })), )