use super::r#const::*;
use super::*;
const UNLOCK_KEY_PREFIX: &str = "euv-docs:unlocked:";
async fn async_sha256_hex(input: &str) -> Option<String> {
let escaped: String = input
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r");
let script: String = format!(
r#"(async () => {{
const data = new TextEncoder().encode("{escaped}");
const buf = await crypto.subtle.digest("SHA-256", data);
const bytes = new Uint8Array(buf);
let out = "";
for (let i = 0; i < bytes.length; i++) {{
out += bytes[i].toString(16).padStart(2, "0");
}}
return out;
}})()"#
);
let value: JsValue = js_sys::eval(&script).ok()?;
let promise: js_sys::Promise = value.unchecked_into();
let result: Result<JsValue, JsValue> =
wasm_bindgen_futures::JsFuture::from(promise).await.into();
let digest: String = result.ok()?.as_string()?;
Some(digest)
}
fn unlock_key_for(route: &str) -> String {
let mut out: String = String::with_capacity(UNLOCK_KEY_PREFIX.len() + route.len());
out.push_str(UNLOCK_KEY_PREFIX);
for c in route.chars() {
if c.is_ascii_alphanumeric() {
out.push(c);
} else {
out.push('-');
}
}
out
}
#[component]
pub(crate) fn docs_password_gate(node: VirtualNode<DocsPasswordGateProps>) -> VirtualNode {
let DocsPasswordGateProps {
route,
expected_hash,
title,
}: DocsPasswordGateProps = node.try_get_props().unwrap_or_default();
let input_id: String = format!("pw-gate-{route}");
let unlock_key: String = unlock_key_for(route);
let input_signal: Signal<String> = App::use_signal(String::new);
let error_signal: Signal<String> = App::use_signal(String::new);
let busy_signal: Signal<bool> = App::use_signal(|| false);
let submit = submit_handler(
route,
expected_hash,
unlock_key,
input_signal,
error_signal,
busy_signal,
);
let oninput = oninput_handler(input_signal);
let onkeydown = onkeydown_handler(submit.clone());
html! {
div {
class: c_pw_gate_wrapper()
div {
class: c_pw_gate_card()
h2 {
class: c_pw_gate_title()
{
title
}
}
p {
class: c_pw_gate_hint()
"This article is password-protected. Enter the passphrase to view its contents."
}
input {
id: input_id.clone()
type: "password"
placeholder: "Password"
autocomplete: "off"
class: if { !error_signal.get().is_empty() } {
c_pw_gate_input_error()
} else {
c_pw_gate_input()
}
value: input_signal.get()
oninput: oninput
onkeydown: onkeydown
}
if { !error_signal.get().is_empty() } {
p {
class: c_pw_gate_error()
error_signal.get()
}
}
button {
class: c_pw_gate_submit()
disabled: busy_signal.get()
onclick: submit
if { busy_signal.get() } {
"Verifying…"
} else {
"Unlock"
}
}
}
}
}
}
fn submit_handler(
route: &'static str,
expected_hash: &'static str,
unlock_key: String,
input_signal: Signal<String>,
error_signal: Signal<String>,
busy_signal: Signal<bool>,
) -> Option<Rc<dyn Fn(Event)>> {
Some(Rc::new(move |_event: Event| {
let typed: String = input_signal.get();
if typed.is_empty() || busy_signal.get() {
return;
}
error_signal.set(String::new());
busy_signal.set(true);
let route_static: &'static str = route;
let unlock_key_owned: String = unlock_key.clone();
let expected_hash_owned: &'static str = expected_hash;
wasm_bindgen_futures::spawn_local(async move {
let digest: Option<String> = async_sha256_hex(&typed).await;
let matched: bool = digest
.as_deref()
.is_some_and(|computed: &str| computed == expected_hash_owned);
if matched {
UseEuvBrowser::local_storage_set(&unlock_key_owned, "1");
if let Some(window) = web_sys::window() {
let location: web_sys::Location = window.location();
let current: String = location.hash().unwrap_or_default();
let next: String = if current.is_empty() {
format!("#{route_static}")
} else {
current
};
let _: Result<(), JsValue> = location.set_hash(&next);
}
input_signal.set(String::new());
error_signal.set(String::new());
} else {
error_signal.set("Incorrect password.".to_string());
input_signal.set(String::new());
}
busy_signal.set(false);
});
}))
}
fn oninput_handler(input_signal: Signal<String>) -> Option<Rc<dyn Fn(Event)>> {
Some(Rc::new(move |event: Event| {
if let Some(input) = event
.target()
.and_then(|t| t.dyn_into::<web_sys::HtmlInputElement>().ok())
{
input_signal.set(input.value());
}
}))
}
fn onkeydown_handler(submit: Option<Rc<dyn Fn(Event)>>) -> Option<Rc<dyn Fn(Event)>> {
Some(Rc::new(move |event: Event| {
if let Some(keyboard) = event.dyn_ref::<web_sys::KeyboardEvent>() {
if keyboard.key() == "Enter" {
if let Some(handler) = submit.as_ref() {
handler.as_ref()(event);
}
}
}
}))
}
pub(crate) fn is_unlocked(route: &str) -> bool {
let key: String = unlock_key_for(route);
UseEuvBrowser::local_storage_get(&key).as_deref() == Some("1")
}