feat: implemented signup and account verification
This commit is contained in:
parent
6c915ef56d
commit
35d0ca8945
|
@ -5,5 +5,6 @@
|
|||
<title>Nerdcult</title>
|
||||
<link data-trunk rel="css" href="style/topbar.css">
|
||||
<link data-trunk rel="css" href="style/home.css">
|
||||
<link data-trunk rel="css" href="style/components.css">
|
||||
</head>
|
||||
</html>
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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<Result<()>>,
|
||||
) {
|
||||
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::<data::AccountRegisterConflictResponse>()
|
||||
.await?;
|
||||
|
||||
Err(Error::msg(format!(
|
||||
"The requested {} is already taken.",
|
||||
conflict
|
||||
)))
|
||||
}
|
||||
422 => {
|
||||
let problem = response
|
||||
.json::<data::AccountRegisterCriteriaProblemResponse>()
|
||||
.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<Result<()>>) {
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
<SignUp/>
|
||||
<SignUp close={sign_up_close}/>
|
||||
}
|
||||
</nav>
|
||||
}
|
||||
|
|
|
@ -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::<Session>().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::<HtmlInputElement>()
|
||||
.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! {
|
||||
<form class="auth-container" onsubmit={onsubmit}>
|
||||
<div class="auth-field">
|
||||
<label for="username">{"Username"}</label><br/>
|
||||
<input class="auth-input" type="text" name="username" onchange={on_username_changed}/><br/>
|
||||
</div>
|
||||
let cloned_state = state.clone();
|
||||
let on_token_changed = Callback::from(move |event: Event| {
|
||||
let token = event
|
||||
.target()
|
||||
.expect("WTF")
|
||||
.unchecked_into::<HtmlInputElement>()
|
||||
.value();
|
||||
|
||||
<div class="auth-field">
|
||||
<label for="password">{"Password"}</label><br/>
|
||||
<input class="auth-input" type="password" name="password" onchange={on_password_changed}/><br/>
|
||||
</div>
|
||||
let mut data = cloned_state.deref().clone();
|
||||
data.token = token;
|
||||
cloned_state.set(data);
|
||||
});
|
||||
|
||||
<div class="auth-field">
|
||||
<label for="email">{"Email"}</label><br/>
|
||||
<input class="auth-input" type="text" name="email" onchange={on_email_changed}/><br/>
|
||||
</div>
|
||||
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;
|
||||
|
||||
<div class="center-x">
|
||||
<button class="auth-submit" type="submit">{"Sign In"}</button>
|
||||
</div>
|
||||
</form>
|
||||
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! {
|
||||
<form class="auth-container" onsubmit={onsubmit}>
|
||||
<div class="auth-field">
|
||||
<label for="username">{"Username"}</label><br/>
|
||||
<input class="auth-input" type="text" name="username" onchange={on_username_changed}/><br/>
|
||||
</div>
|
||||
|
||||
<div class="auth-field">
|
||||
<label for="email">{"Email"}</label><br/>
|
||||
<input class="auth-input" type="text" name="email" onchange={on_email_changed}/><br/>
|
||||
</div>
|
||||
|
||||
<div class="auth-field">
|
||||
<label for="password">{"Password"}</label><br/>
|
||||
<input class="auth-input" type="password" name="password" onchange={on_password_changed}/><br/>
|
||||
</div>
|
||||
|
||||
<div class="auth-field">
|
||||
<label for="password-reenter">{"Re Enter Password"}</label><br/>
|
||||
<input class="auth-input" type="password" name="password-reenter" onchange={on_password_reenter_changed}/><br/>
|
||||
</div>
|
||||
|
||||
if !state.submitted {
|
||||
<div class="center-x">
|
||||
<button class="auth-submit" type="submit">{"Sign Up"}</button>
|
||||
</div>
|
||||
} else {
|
||||
<div class="center-x">
|
||||
<div class="spinner" />
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="center-x">
|
||||
<p class="auth-status">{&state.clone().deref().status}</p>
|
||||
</div>
|
||||
</form>
|
||||
},
|
||||
View::Verify => html! {
|
||||
<form class="auth-container" onsubmit={onsubmit}>
|
||||
<div class="auth-field">
|
||||
<label for="token">{"Verification Token"}</label><br/>
|
||||
<input class="auth-input" type="text" name="token" onchange={on_token_changed}/><br/>
|
||||
</div>
|
||||
|
||||
if !state.submitted {
|
||||
<div class="center-x">
|
||||
<button class="auth-submit" type="submit">{"Verify"}</button>
|
||||
</div>
|
||||
} else {
|
||||
<div class="center-x">
|
||||
<div class="spinner" />
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="center-x">
|
||||
<p class="auth-status">{&state.clone().deref().status}</p>
|
||||
</div>
|
||||
</form>
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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); }
|
||||
}
|
Loading…
Reference in New Issue