diff options
Diffstat (limited to 'web/pwcrypt/src')
-rw-r--r-- | web/pwcrypt/src/main.html | 48 | ||||
-rw-r--r-- | web/pwcrypt/src/main.rs | 160 |
2 files changed, 208 insertions, 0 deletions
diff --git a/web/pwcrypt/src/main.html b/web/pwcrypt/src/main.html new file mode 100644 index 000000000000..b19cbd379b70 --- /dev/null +++ b/web/pwcrypt/src/main.html @@ -0,0 +1,48 @@ +html!{ +<div class="container"> + <h1>{"//web/pwcrypt"}</h1> + <p>{"You can use this page to create your hashed credentials for a TVL account. Enter your desired username and password below, and send us the output you receive in order for us to create your account."}</p> + <p> + {"Detailed documentation about the registration process is "} + <a href="https://code.tvl.fyi/about/docs/REVIEWS.md#registration"> + {"available here"} + </a> + {"."} + </p> + <p>{"All of this happens in your browser: Your password does not leave this site!"}</p> + + <form> + <fieldset> + <legend>{"Credentials:"}</legend> + + <div class="form-group"> + <label for="username">{"Username:"}</label> + <input id="username" name="username" type="text" + oninput={link.callback(|event| input_to_message(event, Msg::SetUsername))} /> + </div> + + <div class="form-group"> + <label for="email">{"Email:"}</label> + <input id="email" name="email" type="email" + oninput={link.callback(|event| input_to_message(event, Msg::SetEmail))} /> + </div> + + <div class="form-group"> + <label for="password">{"Password:"}</label> + <input id="password" name="password" type="password" + oninput={link.callback(|event| input_to_message(event, Msg::SetPassword))} /> + </div> + + if let Some(missing) = self.whats_missing() { + <p>{"Please fill in "}{missing}{"."}</p> + } else { + <div class="form-group"> + <button class="btn btn-default" type="button" + onclick={link.callback(|_| Msg::UpdateCredentials)}>{"Prepare credentials"}</button> + </div> + } + </fieldset> + </form> + {self.display_credentials()} +</div> +} diff --git a/web/pwcrypt/src/main.rs b/web/pwcrypt/src/main.rs new file mode 100644 index 000000000000..2b9b82abb022 --- /dev/null +++ b/web/pwcrypt/src/main.rs @@ -0,0 +1,160 @@ +use argon2::password_hash::{PasswordHasher, SaltString}; +use argon2::Argon2; +use gloo::console::log; +use rand_core::OsRng; +use web_sys::HtmlInputElement; +use yew::prelude::*; + +fn hash_password(pw: &str) -> String { + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::default(); + argon2 + .hash_password(pw.as_bytes(), &salt) + .expect("failed to hash pw") + .to_string() +} + +enum Msg { + NoOp, + SetEmail(String), + SetPassword(String), + SetUsername(String), + UpdateCredentials, +} + +#[derive(Default)] +struct App { + email: Option<String>, + password: Option<String>, + username: Option<String>, + hashed: Option<String>, +} + +impl App { + fn whats_missing(&self) -> Option<String> { + let mut missing = vec![]; + + if self.username.is_none() { + missing.push("username"); + } + + if self.email.is_none() { + missing.push("email"); + } + + if self.password.is_none() { + missing.push("password"); + } + + match missing.len() { + 0 => None, + 1 => Some(missing[0].to_string()), + 2 => Some(format!("{} and {}", missing[0], missing[1])), + 3 => Some(format!("{}, {} and {}", missing[0], missing[1], missing[2])), + _ => unreachable!(), + } + } + + fn update_credentials(&mut self) { + if self.password.is_none() { + log!("error: password unset, but credentials requested"); + return; + } + + let pw = self.password.as_ref().unwrap(); + let hashed = hash_password(pw); + log!("hashed password to", &hashed); + self.hashed = Some(hashed); + } + + fn display_credentials(&self) -> Html { + if let (Some(username), Some(email), Some(hash)) = + (&self.username, &self.email, &self.hashed) + { + html! { + <> + <hr /> + <p>{"Your credentials are as follows: "}</p> + <pre> + {" {\n"} + {" username = \""}{username}{"\";\n"} + {" email = \""}{email}{"\";\n"} + {" password = \"{ARGON2}"}{hash}{"\";\n"} + {" }"} + </pre> + <p> + {"Please propose a CL to "} + <a href="https://at.tvl.fyi/?q=//ops/users/default.nix"> + <code>{"//ops/users/default.nix"}</code> + </a> + {", or submit your patch via email to "} + <a href="mailto:depot@tvl.su">{"depot@tvl.su"}</a> + {"."} + </p> + </> + } + } else { + html! {} + } + } +} + +fn input_to_message(event: InputEvent, msg: fn(String) -> Msg) -> Msg { + let input = event.target_unchecked_into::<HtmlInputElement>(); + if input.check_validity() { + msg(input.value()) + } else { + Msg::NoOp + } +} + +fn set_if_present(s: String, target: &mut Option<String>) { + if s.is_empty() { + *target = None; + } else { + *target = Some(s); + } +} + +impl Component for App { + type Message = Msg; + type Properties = (); + + fn create(_: &Context<Self>) -> Self { + Self::default() + } + + fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool { + log!( + "handling message ", + match msg { + Msg::NoOp => "NoOp", + Msg::SetEmail(_) => "SetEmail", + Msg::SetUsername(_) => "SetUsername", + Msg::SetPassword(_) => "SetPassword", + Msg::UpdateCredentials => "UpdateCredentials", + } + ); + + match msg { + Msg::NoOp => return false, + Msg::SetEmail(email) => set_if_present(email, &mut self.email), + Msg::SetUsername(username) => set_if_present(username, &mut self.username), + Msg::SetPassword(password) => set_if_present(password, &mut self.password), + Msg::UpdateCredentials => { + self.update_credentials(); + } + } + + true + } + + fn view(&self, ctx: &Context<Self>) -> Html { + let link = ctx.link(); + include!("main.html") + } +} + +fn main() { + yew::Renderer::<App>::new().render(); +} |