chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
The page after a CLI login looks like knotless and says when it failed
.changeset/login-page.mdnew+7−0
| @@ -0,0 +1,7 @@ | ||
--- | ||
default: patch | ||
--- | ||
| ||
# The page after logging in is easier to read | ||
| ||
The page the browser shows once you have approved a login now has a heading and the look of knotless's own pages, in light and dark, and says so when the server refused the login rather than that you are logged in. | ||
crates/knotless/src/oauth.rs+45−9
| @@ -680,9 +680,48 @@ pub async fn start(ctx: &Ctx, identifier: &str) -> Result<Pending> { | ||
}) | ||
} | ||
| ||
/// What the browser shows once it is back. | ||
const DONE: &str = "<!doctype html><meta charset=utf-8><title>knotless</title>\ | ||
<p>Back to knotless: you can close this tab and return to your terminal."; | ||
/// What the browser shows once it is back: a small page of its own, with | ||
/// its styles inline and the palette of knotless's pages, as the browser | ||
/// is at our own loopback address and asks nothing of anyone else. | ||
fn page(heading: &str, line: &str) -> String { | ||
format!( | ||
"<!doctype html><html lang=en><meta charset=utf-8>\ | ||
<meta name=viewport content=\"width=device-width,initial-scale=1\">\ | ||
<title>knotless</title>\ | ||
<style>\ | ||
:root{{color-scheme:light dark;--bg:#fff;--panel:#fafafa;--line:#e8e8e8;--fg:#171717;--mute:#525252}}\ | ||
@media(prefers-color-scheme:dark){{:root{{--bg:#111;--panel:#161616;--line:#262626;--fg:#ededed;--mute:#a1a1a1}}}}\ | ||
body{{margin:0;min-height:100vh;display:grid;place-items:center;padding:1rem;box-sizing:border-box;\ | ||
background:var(--bg);color:var(--fg);font:1rem/1.5 system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue',sans-serif}}\ | ||
main{{max-width:24rem;padding:2rem;text-align:center;background:var(--panel);border:1px solid var(--line);border-radius:6px}}\ | ||
b{{display:block;margin-bottom:1.5rem;font:600 1rem ui-monospace,SFMono-Regular,Menlo,monospace}}\ | ||
h1{{margin:0 0 .5rem;font-size:1.25rem;font-weight:600}}\ | ||
p{{margin:0;color:var(--mute)}}\ | ||
</style>\ | ||
<main><b>~ knotless</b><h1>{heading}</h1><p>{line}</p></main>" | ||
) | ||
} | ||
| ||
/// The reply to a browser that came back with `params`: the login's, or | ||
/// the server's word that it was refused. | ||
fn reply(params: &HashMap<String, String>) -> String { | ||
let body = if params.contains_key("error") { | ||
page( | ||
"The login did not go through", | ||
"Go back to your terminal to see why, then try again.", | ||
) | ||
} else { | ||
page( | ||
"You are logged in", | ||
"You can close this tab and go back to your terminal.", | ||
) | ||
}; | ||
format!( | ||
"HTTP/1.1 200 OK\r\ncontent-type: text/html; charset=utf-8\r\n\ | ||
content-length: {}\r\nconnection: close\r\n\r\n{body}", | ||
body.len() | ||
) | ||
} | ||
| ||
impl Pending { | ||
/// Waits for the browser, trades the code for tokens, and stores the | ||
| @@ -718,13 +757,10 @@ impl Pending { | ||
.await?; | ||
continue; | ||
}; | ||
let reply = format!( | ||
"HTTP/1.1 200 OK\r\ncontent-type: text/html; charset=utf-8\r\n\ | ||
content-length: {}\r\nconnection: close\r\n\r\n{DONE}", | ||
DONE.len() | ||
); | ||
let params: HashMap<String, String> = url.query_pairs().into_owned().collect(); | ||
let reply = reply(¶ms); | ||
stream.write_all(reply.as_bytes()).await?; | ||
return Ok(url.query_pairs().into_owned().collect()); | ||
return Ok(params); | ||
} | ||
} | ||
} | ||
crates/knotless/tests/oauth.rs+26−1
| @@ -201,7 +201,32 @@ fn the_listener_takes_only_its_own_callback() { | ||
.send() | ||
.await | ||
.unwrap(); | ||
assert!(page.text().await.unwrap().contains("knotless")); | ||
let page = page.text().await.unwrap(); | ||
assert!(page.contains("You are logged in"), "{page}"); | ||
assert!(page.contains("prefers-color-scheme"), "{page}"); | ||
}); | ||
let res = pending.finish(&ctx).await; | ||
poke.await.unwrap(); | ||
res | ||
}) | ||
.unwrap_err(); | ||
assert!(err.to_string().contains("did not start"), "{err}"); | ||
} | ||
| ||
#[test] | ||
fn the_page_says_when_the_server_refused_the_login() { | ||
let s = oauth_sandbox(account(), ""); | ||
let ctx = Ctx::new(s.env()); | ||
let err = rt() | ||
.block_on(async { | ||
let pending = oauth::start(&ctx, &s.account.handle).await?; | ||
let back = pending.redirect_uri.clone(); | ||
let poke = tokio::spawn(async move { | ||
let page = reqwest::get(format!("{back}?error=access_denied")) | ||
.await | ||
.unwrap(); | ||
let page = page.text().await.unwrap(); | ||
assert!(page.contains("did not go through"), "{page}"); | ||
}); | ||
let res = pending.finish(&ctx).await; | ||
poke.await.unwrap(); | ||