From 35d0ca894555b49d3c7828b4ef46d1a11924b17f Mon Sep 17 00:00:00 2001 From: antifallobst Date: Fri, 6 Oct 2023 20:48:56 +0200 Subject: [PATCH] feat: implemented signup and account verification --- index.html | 1 + src/backend/data.rs | 28 ++++++ src/backend/mod.rs | 87 ++++++++++++++++++- src/topbar/guest/mod.rs | 9 +- src/topbar/guest/signup.rs | 172 +++++++++++++++++++++++++++++++------ style/components.css | 15 ++++ 6 files changed, 283 insertions(+), 29 deletions(-) create mode 100644 style/components.css diff --git a/index.html b/index.html index eea29aa..b72734b 100644 --- a/index.html +++ b/index.html @@ -5,5 +5,6 @@ Nerdcult + diff --git a/src/backend/data.rs b/src/backend/data.rs index 7a4ca62..106d7b0 100644 --- a/src/backend/data.rs +++ b/src/backend/data.rs @@ -1,6 +1,34 @@ use serde::Deserialize; +use std::fmt::{Display, Formatter}; #[derive(Deserialize)] pub struct AccountAuthenticateResponse { pub token: String, } + +#[derive(Deserialize)] +#[serde(tag = "conflict", rename_all = "snake_case")] +pub enum AccountRegisterConflictResponse { + Username, + Email, +} + +impl Display for AccountRegisterConflictResponse { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Username => "username", + Self::Email => "email", + } + ) + } +} + +#[derive(Deserialize)] +#[serde(tag = "problem", rename_all = "snake_case")] +pub enum AccountRegisterCriteriaProblemResponse { + Email, + Password, +} diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 00c1660..5a47bda 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -39,7 +39,9 @@ pub struct Session { impl Default for Session { fn default() -> Self { Self { - base_url: option_env!("NC_API_URL").unwrap_or("https://api.nerdcult.net").to_string(), + base_url: option_env!("NC_API_URL") + .unwrap_or("https://api.nerdcult.net") + .to_string(), auth_token: get_auth_cookie(), } } @@ -92,6 +94,89 @@ impl Session { ); } + pub fn register( + &self, + username: String, + password: String, + email: String, + callback: Callback>, + ) { + let url = format!("{}/account/register", &self.base_url); + + let body = json!({ + "username": username, + "password": password, + "email": email + }); + + let call = async move { + let request = Request::post(&url) + .mode(RequestMode::Cors) + .credentials(RequestCredentials::Omit) + .json(&body)?; + let response = request.send().await?; + + match response.status() { + 200 => Ok(()), + 400 => Err(Error::msg(format!("Bad request"))), + 403 => Err(Error::msg(format!("You're not allowed to do this!"))), + 409 => { + let conflict = response + .json::() + .await?; + + Err(Error::msg(format!( + "The requested {} is already taken.", + conflict + ))) + } + 422 => { + let problem = response + .json::() + .await?; + + match problem { + data::AccountRegisterCriteriaProblemResponse::Email => { + Err(Error::msg(format!("Not a valid email!"))) + } + data::AccountRegisterCriteriaProblemResponse::Password => Err(Error::msg( + format!("The password does not meet the criteria!"), + )), + } + } + e => Err(Error::msg(format!("Unknown response: {e}"))), + } + }; + + wasm_bindgen_futures::spawn_local(async move { callback.emit(call.await) }); + } + + pub fn verify(&self, token: String, callback: Callback>) { + let url = format!("{}/account/verify", &self.base_url); + + let body = json!({ + "token": token + }); + + let call = async move { + let request = Request::post(&url) + .mode(RequestMode::Cors) + .credentials(RequestCredentials::Omit) + .json(&body)?; + let response = request.send().await?; + + match response.status() { + 200 => Ok(()), + 400 => Err(Error::msg(format!("Bad request"))), + 403 => Err(Error::msg(format!("You're not allowed to do this!"))), + 404 => Err(Error::msg(format!("Unknown Token!"))), + e => Err(Error::msg(format!("Unknown response: {e}"))), + } + }; + + wasm_bindgen_futures::spawn_local(async move { callback.emit(call.await) }); + } + pub fn set_token(&mut self, token: String) { self.auth_token = Some(token); } diff --git a/src/topbar/guest/mod.rs b/src/topbar/guest/mod.rs index 107cb3f..49f4568 100644 --- a/src/topbar/guest/mod.rs +++ b/src/topbar/guest/mod.rs @@ -20,7 +20,6 @@ pub struct State { sign_up: bool, sign_in_status: AttrValue, - sign_up_status: AttrValue, } impl Default for State { @@ -30,7 +29,6 @@ impl Default for State { sign_up: false, sign_in_status: AttrValue::default(), - sign_up_status: AttrValue::default(), } } } @@ -74,6 +72,11 @@ pub fn TopBar() -> Html { Callback::from(move |_| state.dispatch(Action::ToggleSignUp)) }; + let sign_up_close = { + let state = state.clone(); + Callback::from(move |_| state.dispatch(Action::ToggleSignUp)) + }; + let sign_in_status = { let state = state.clone(); Callback::from(move |status: Result<()>| match status { @@ -101,7 +104,7 @@ pub fn TopBar() -> Html { } if state.sign_up { - + } } diff --git a/src/topbar/guest/signup.rs b/src/topbar/guest/signup.rs index fc27b9d..8bbe60a 100644 --- a/src/topbar/guest/signup.rs +++ b/src/topbar/guest/signup.rs @@ -1,18 +1,45 @@ +use crate::backend::Session; +use anyhow::Result; use std::ops::Deref; use wasm_bindgen::JsCast; use web_sys::HtmlInputElement; use yew::prelude::*; +#[derive(PartialEq, Clone, Debug)] +enum View { + SignUp, + Verify, +} + +impl Default for View { + fn default() -> Self { + Self::SignUp + } +} + #[derive(PartialEq, Clone, Default, Debug)] struct Data { + pub view: View, + pub status: String, + pub submitted: bool, + pub username: String, pub email: String, pub password: String, + pub password_reenter: String, + + pub token: String, +} + +#[derive(Properties, PartialEq)] +pub struct Props { + pub close: Callback<()>, } #[function_component] -pub fn SignUp() -> Html { +pub fn SignUp(props: &Props) -> Html { let state = use_state(|| Data::default()); + let session = use_context::().unwrap_or_default(); let cloned_state = state.clone(); let on_username_changed = Callback::from(move |event: Event| { @@ -54,35 +81,130 @@ pub fn SignUp() -> Html { }); let cloned_state = state.clone(); - let onsubmit = Callback::from(move |event: SubmitEvent| { - event.prevent_default(); + let on_password_reenter_changed = Callback::from(move |event: Event| { + let password_reenter = event + .target() + .expect("WTF") + .unchecked_into::() + .value(); - let data = cloned_state.deref().clone(); - gloo::console::log!("username: ", data.username); - gloo::console::log!("email: ", data.email); - gloo::console::log!("password: ", data.password); + let mut data = cloned_state.deref().clone(); + data.password_reenter = password_reenter; + cloned_state.set(data); }); - html! { -
-
-
-
-
+ let cloned_state = state.clone(); + let on_token_changed = Callback::from(move |event: Event| { + let token = event + .target() + .expect("WTF") + .unchecked_into::() + .value(); -
-
-
-
+ let mut data = cloned_state.deref().clone(); + data.token = token; + cloned_state.set(data); + }); -
-
-
-
+ let cloned_state = state.clone(); + let callback = props.close.clone(); + let on_api_response = Callback::from(move |response: Result<()>| { + let mut state = cloned_state.deref().clone(); + state.submitted = false; -
- -
- + match state.view { + View::SignUp => match response { + Ok(_) => state.view = View::Verify, + Err(e) => state.status = e.to_string(), + }, + View::Verify => match response { + Ok(_) => callback.emit(()), + Err(e) => state.status = e.to_string(), + }, + } + + cloned_state.set(state); + }); + + let cloned_state = state.clone(); + let onsubmit = Callback::from(move |event: SubmitEvent| { + event.prevent_default(); + let mut state = cloned_state.deref().clone(); + state.status = String::default(); + state.submitted = true; + + match state.view { + View::SignUp => session.register( + state.username.clone(), + state.password.clone(), + state.email.clone(), + on_api_response.clone(), + ), + View::Verify => session.verify(state.token.clone(), on_api_response.clone()), + } + + cloned_state.set(state); + }); + + match state.view { + View::SignUp => html! { +
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ + if !state.submitted { +
+ +
+ } else { +
+
+
+ } + +
+

{&state.clone().deref().status}

+
+ + }, + View::Verify => html! { +
+
+
+
+
+ + if !state.submitted { +
+ +
+ } else { +
+
+
+ } + +
+

{&state.clone().deref().status}

+
+ + }, } } diff --git a/style/components.css b/style/components.css new file mode 100644 index 0000000..01c0c45 --- /dev/null +++ b/style/components.css @@ -0,0 +1,15 @@ +.spinner { + display: inline-block; + top: 50%; + border: 8px solid #00000000; + border-top: 8px solid #690455; + border-radius: 50%; + width: 32px; + height: 32px; + animation: spin 1.5s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} \ No newline at end of file