Replace JS template interpolation with DOM APIs

Use serde_json::to_string() to safely encode Rust strings as JS string
literals and DOM element creation (.textContent, .value) on the JS side
to insert into the page.
This commit is contained in:
Adrian Kumpf
2026-02-25 14:25:23 +01:00
parent f52b423d40
commit 9bbdddc03b
3 changed files with 58 additions and 31 deletions
Generated
-1
View File
@@ -2852,7 +2852,6 @@ dependencies = [
"muda", "muda",
"oauth2", "oauth2",
"reqwest 0.13.2", "reqwest 0.13.2",
"serde",
"serde_json", "serde_json",
"simple_logger", "simple_logger",
"static_vcruntime", "static_vcruntime",
-1
View File
@@ -22,7 +22,6 @@ reqwest = { version = "0.13.2", default-features = false, features = [
"json", "json",
"rustls", "rustls",
] } ] }
serde = { version = "1.0.228", default-features = false, features = ["derive"] }
serde_json = "1.0.149" serde_json = "1.0.149"
simple_logger = { version = "5.2.0", default-features = false, features = [ simple_logger = { version = "5.2.0", default-features = false, features = [
"stderr", "stderr",
+58 -29
View File
@@ -168,12 +168,16 @@ fn main() -> anyhow::Result<()> {
Event::UserEvent(UserEvent::Failure(error)) => { Event::UserEvent(UserEvent::Failure(error)) => {
log::error!("{error}"); log::error!("{error}");
webview.evaluate_script(&render_error_view(error)).unwrap(); if let Err(e) = webview.evaluate_script(&render_error_view(error)) {
log::error!("Failed to render error view: {e}");
}
} }
Event::UserEvent(UserEvent::Tokens(token)) => { Event::UserEvent(UserEvent::Tokens(token)) => {
println!("{token}"); println!("{token}");
webview.evaluate_script(&render_tokens_view(token)).unwrap(); if let Err(e) = webview.evaluate_script(&render_tokens_view(token)) {
log::error!("Failed to render tokens view: {e}");
}
} }
Event::UserEvent(UserEvent::LoginCanceled) => { Event::UserEvent(UserEvent::LoginCanceled) => {
@@ -244,36 +248,61 @@ fn handle_redirect(url: &Url, client: auth::Client) -> UserEvent {
} }
} }
// Encode a string as a JSON string literal for safe JS interpolation.
#[expect(clippy::unwrap_used)] // serde_json string serialization is infallible
fn js_string(s: &str) -> String {
serde_json::to_string(s).unwrap()
}
fn render_error_view(error: anyhow::Error) -> String { fn render_error_view(error: anyhow::Error) -> String {
r#" let msg = js_string(&error.to_string());
const html = ` format!(
<h4 style="text-align: center;">An error occurred. Please try again ...</h4> r#"(function() {{
<p style="text-align: center;color:red;margin-bottom:20px;">{msg}</p> var target = document.querySelector("h1.h1");
`; var h4 = document.createElement("h4");
document.querySelector("h1.h1").outerHTML = html; h4.style.textAlign = "center";
"# h4.textContent = "An error occurred. Please try again ...";
.replace("{msg}", &error.to_string()) var p = document.createElement("p");
p.style.cssText = "text-align:center;color:red;margin-bottom:20px";
p.textContent = {msg};
target.replaceWith(h4, p);
}})()"#
)
} }
fn render_tokens_view(tokens: auth::Tokens) -> String { fn render_tokens_view(tokens: auth::Tokens) -> String {
r#" let access = js_string(tokens.access.secret());
const html = ` let refresh = js_string(tokens.refresh.secret());
<h4 style="text-align: center;">Access Token</h4> let expires = js_string(&tokens.expires_in.to_string());
<textarea readonly onclick="this.setSelectionRange(0, this.value.length)" format!(
cols="100" rows="12" style="resize:none;padding:4px;font-size:0.9em;" r#"(function() {{
>{access_token}</textarea> var target = document.querySelector("h1.h1");
<h4 style="text-align: center;">Refresh Token</h4> var frag = document.createDocumentFragment();
<textarea readonly onclick="this.setSelectionRange(0, this.value.length)"
cols="100" rows="12" style="resize:none;padding:4px;font-size:0.9em;"
>{refresh_token}</textarea>
<small style="margin-top:12px;margin-bottom:20px;text-align:center;color:seagreen;">
Valid for {expires_in}
</small>
`;
document.querySelector("h1.h1").outerHTML = html; function addToken(label, value) {{
"# var h4 = document.createElement("h4");
.replace("{access_token}", tokens.access.secret()) h4.style.textAlign = "center";
.replace("{refresh_token}", tokens.refresh.secret()) h4.textContent = label;
.replace("{expires_in}", &format!("{}", tokens.expires_in)) frag.appendChild(h4);
var ta = document.createElement("textarea");
ta.readOnly = true;
ta.cols = 100;
ta.rows = 12;
ta.style.cssText = "resize:none;padding:4px;font-size:0.9em";
ta.value = value;
ta.addEventListener("click", function() {{ this.setSelectionRange(0, this.value.length); }});
frag.appendChild(ta);
}}
addToken("Access Token", {access});
addToken("Refresh Token", {refresh});
var small = document.createElement("small");
small.style.cssText = "margin-top:12px;margin-bottom:20px;text-align:center;color:seagreen";
small.textContent = "Valid for " + {expires};
frag.appendChild(small);
target.replaceWith(frag);
}})()"#
)
} }