Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/crates/services/miniapp-market-service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
6 changes: 3 additions & 3 deletions src/crates/services/miniapp-market-service/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,9 +312,9 @@ fn user_from_row(row: sqlx::sqlite::SqliteRow) -> AuthenticatedUser {
None => format!("email-{}", row.get::<i64, _>("id")),
}),
github_id: row.get::<Option<i64>, _>("github_id").unwrap_or_default(),
login: row
.get::<Option<String>, _>("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"),
},
}
Expand Down
10 changes: 8 additions & 2 deletions src/crates/services/miniapp-market-service/src/email_auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
16 changes: 15 additions & 1 deletion src/crates/services/skin-market-service/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ pub(crate) struct AuthenticatedIdentity {
#[serde(rename_all = "camelCase")]
struct IdentityResponse {
user: AppearanceMarketUserSummary,
#[serde(default)]
email: Option<String>,
is_admin: bool,
}

Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/crates/services/skin-market-service/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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
})),
)
Expand Down
Loading