Web client: add a top-bar log in / log out button

Relying on the proactive prompt alone was not enough: it only fires on
first connect, only when no credentials are stored, and only against an
auth-enabled server, so a manual affordance was missing.

The top bar now shows a "log in" button whenever the server reports auth
is enabled (Init.auth_enabled, kept in a store signal), opening the same
dismissible credentials dialog. Once credentials are stored it becomes
"log out", which clears them and reloads to drop back to the guest role.
The button is hidden on servers with no auth, where sending credentials
would only earn an UNAUTHENTICATED lock-out.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Test User 2026-07-23 18:27:13 +02:00
parent 659e678522
commit 42c5e9dbc2
1 changed files with 51 additions and 4 deletions

View File

@ -80,6 +80,9 @@ struct Store {
/// Set once the proactive "auth is enabled" prompt has been shown, so
/// stream reconnects do not re-open it after the user dismissed it.
login_prompted: RwSignal<bool>,
/// Whether the server has auth configured (from `Init.auth_enabled`),
/// so the top bar can offer a login only where it is meaningful.
auth_enabled: RwSignal<bool>,
queue: RwSignal<Vec<Track>>,
queue_pos: RwSignal<u32>,
resolving: RwSignal<bool>,
@ -110,6 +113,7 @@ impl Store {
needs_login: RwSignal::new(false),
guest_ok: RwSignal::new(false),
login_prompted: RwSignal::new(false),
auth_enabled: RwSignal::new(false),
queue: RwSignal::new(Vec::new()),
queue_pos: RwSignal::new(0),
resolving: RwSignal::new(false),
@ -530,10 +534,12 @@ fn run_stream(store: Store) {
store.apply(StreamUpdate::Position(position));
}
// We connected — as the unauthenticated fallback
// role if we sent no credentials. When the server
// has auth configured and the user has none stored,
// offer a login once, but let them dismiss it to
// stay on the fallback role.
// role if we sent no credentials. Record whether
// the server has auth at all (drives the top-bar
// login button), then, when it does and the user
// has none stored, offer a login once — dismissible
// so they can stay on the fallback role.
store.auth_enabled.set(init.auth_enabled);
let no_stored_creds = load_pref("user").unwrap_or_default().is_empty();
if init.auth_enabled
&& no_stored_creds
@ -666,6 +672,47 @@ fn TopBar(store: Store) -> impl IntoView {
{move || if store.connected.get() { "" } else { "disconnected — reconnecting…" }}
</span>
<span class="topbar-actions">
{move || {
// A login is only meaningful where the server has auth
// (offer "log in"); once credentials are stored, offer
// "log out" to drop back to the guest/fallback role.
let logged_in = !load_pref("user").unwrap_or_default().is_empty();
(store.auth_enabled.get() || logged_in)
.then(|| {
if logged_in {
view! {
<button
class="ghost"
title="log out (return to the guest role)"
on:click=move |_| {
save_pref("user", "");
save_pref("password", "");
if let Some(window) = web_sys::window() {
let _ = window.location().reload();
}
}
>
"log out"
</button>
}
.into_any()
} else {
view! {
<button
class="ghost"
title="log in"
on:click=move |_| {
store.guest_ok.set(true);
store.dialog.set(Some(Dialog::Login));
}
>
"log in"
</button>
}
.into_any()
}
})
}}
<button class="ghost" title="cycle theme (auto/light/dark)" on:click=cycle_theme>
""
</button>