ref:0403191a92ce538cd3469f97437e64e9e92c440b

fix(auth): validate token on login by hitting /users/me

The previous flow pinged the unauthenticated /health endpoint, so a bad or expired token sailed through and showed "Logged in" — only to fail on the next call with 401. Now login does an authenticated GET /users/me. 401/403 returns an error without overwriting the saved config; success greets the user by name. Network errors still fall through with a warning. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SHA: 0403191a92ce538cd3469f97437e64e9e92c440b
Author: Cole Christensen <cole.christensen@gmail.com>
Date: 2026-05-20 17:09
Parents: 0c8d1b0
1 files changed +37 -8
Type
src/commands/auth.rs +37 −8
@@ -96,28 +96,57 @@
}
};
// /health doesn't require auth, so it can't reject a bad token — hit /users/me instead.
let me_url = format!(
"{}/api/v1/users/me",
url.trim_end_matches('/').trim_end_matches("/api/v1")
);
let resp = reqwest::Client::new()
.get(&me_url)
.bearer_auth(&token)
.send()
.await;
let username: Option<String> = match resp {
Ok(r) if r.status().is_success() => {
r.json::<serde_json::Value>().await.ok().and_then(|v| {
v.pointer("/username")
.or_else(|| v.pointer("/user/username"))
.or_else(|| v.pointer("/data/username"))
.and_then(|x| x.as_str().map(String::from))
})
}
Ok(r)
if r.status() == reqwest::StatusCode::UNAUTHORIZED
|| r.status() == reqwest::StatusCode::FORBIDDEN =>
{
// Validate by hitting the health endpoint
let health_url = format!("{}/health", url.trim_end_matches('/'));
let resp = reqwest::get(&health_url).await;
match resp {
Ok(r) if r.status().is_success() => {}
return Err(format!(
"Token rejected by {url} (status {}). Not saved.",
r.status()
)
.into());
}
Ok(r) => {
output::warn(&format!(
"Server returned status {} — saving anyway",
"Server returned status {} when verifying token — saving anyway",
r.status()
));
None
}
Err(e) => {
output::warn(&format!("Could not reach server: {e} — saving anyway"));
None
}
};
}
let mut config = Config::load().unwrap_or_default();
config.server_url = Some(url.clone());
config.token = Some(token);
config.save()?;
output::success(&format!("Logged in to {url}"));
match username {
Some(u) => output::success(&format!("Logged in to {url} as {u}")),
None => output::success(&format!("Logged in to {url}")),
}
output::info(&format!("Config saved to {}", Config::path().display()));
Ok(())