Show the OIDC profile picture with an identicon fallback
CI / Build and push docker image (push) Successful in 2m9s

GET /api/me now carries the profile picture URL when the provider
supplies an absolute http(s) one; the SPA shows it in the lobby navbar
and draws a deterministic identicon from the sub otherwise, falling back
to the identicon when the image fails to load.

The identicon is a 5x5 mirrored grid seeded by an inline FNV-1a hash so
it stays stable across Rust versions and is unit-testable on the host.
The dev mock IdP hands alice and bob a picture claim.
This commit is contained in:
2026-10-15 00:14:17 +08:00
parent a9eb97e6e1
commit 46f10f145a
13 changed files with 312 additions and 28 deletions
+23 -15
View File
@@ -12,6 +12,7 @@ use sycamore::prelude::*;
use sycamore_router::navigate;
use crate::api;
use crate::components::avatar::avatar;
use crate::components::card::card_back;
use crate::components::login::login_card;
use crate::components::toast::toast;
@@ -483,23 +484,30 @@ pub fn LobbyPage() -> View {
Some(Some(_me)) => {
view! {
(crate::components::nav::app_nav("/", move || {
let name = user
.get_clone()
.flatten()
.map(|u| u.name)
let me = user.get_clone().flatten();
let name = me
.as_ref()
.map(|u| u.name.clone())
.unwrap_or_default();
let avatar_view = me
.as_ref()
.map(|u| avatar(&u.sub, u.picture.as_deref()))
.unwrap_or_else(|| view! {});
view! {
span(class="navbar-text me-3") {
"Signed in as " strong { (name) }
(move || ratings
.get_clone()
.into_iter()
.find(|r| r.game_type == selected_game.get_clone())
.map(|r| view! {
span(class="badge text-bg-warning ms-2") {
"Elo " (r.rating)
}
}))
span(class="navbar-text me-3 d-flex align-items-center gap-2") {
(avatar_view)
span() {
"Signed in as " strong { (name) }
(move || ratings
.get_clone()
.into_iter()
.find(|r| r.game_type == selected_game.get_clone())
.map(|r| view! {
span(class="badge text-bg-warning ms-2") {
"Elo " (r.rating)
}
}))
}
}
a(
class="btn btn-sm btn-outline-light",