feat: implemented signup and account verification
This commit is contained in:
parent
6c915ef56d
commit
35d0ca8945
|
@ -5,5 +5,6 @@
|
||||||
<title>Nerdcult</title>
|
<title>Nerdcult</title>
|
||||||
<link data-trunk rel="css" href="style/topbar.css">
|
<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/home.css">
|
||||||
|
<link data-trunk rel="css" href="style/components.css">
|
||||||
</head>
|
</head>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,6 +1,34 @@
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct AccountAuthenticateResponse {
|
pub struct AccountAuthenticateResponse {
|
||||||
pub token: String,
|
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 {
|
impl Default for Session {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
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(),
|
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) {
|
pub fn set_token(&mut self, token: String) {
|
||||||
self.auth_token = Some(token);
|
self.auth_token = Some(token);
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,6 @@ pub struct State {
|
||||||
sign_up: bool,
|
sign_up: bool,
|
||||||
|
|
||||||
sign_in_status: AttrValue,
|
sign_in_status: AttrValue,
|
||||||
sign_up_status: AttrValue,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for State {
|
impl Default for State {
|
||||||
|
@ -30,7 +29,6 @@ impl Default for State {
|
||||||
sign_up: false,
|
sign_up: false,
|
||||||
|
|
||||||
sign_in_status: AttrValue::default(),
|
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))
|
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 sign_in_status = {
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
Callback::from(move |status: Result<()>| match status {
|
Callback::from(move |status: Result<()>| match status {
|
||||||
|
@ -101,7 +104,7 @@ pub fn TopBar() -> Html {
|
||||||
}
|
}
|
||||||
|
|
||||||
if state.sign_up {
|
if state.sign_up {
|
||||||
<SignUp/>
|
<SignUp close={sign_up_close}/>
|
||||||
}
|
}
|
||||||
</nav>
|
</nav>
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,45 @@
|
||||||
|
use crate::backend::Session;
|
||||||
|
use anyhow::Result;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
use web_sys::HtmlInputElement;
|
use web_sys::HtmlInputElement;
|
||||||
use yew::prelude::*;
|
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)]
|
#[derive(PartialEq, Clone, Default, Debug)]
|
||||||
struct Data {
|
struct Data {
|
||||||
|
pub view: View,
|
||||||
|
pub status: String,
|
||||||
|
pub submitted: bool,
|
||||||
|
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
|
pub password_reenter: String,
|
||||||
|
|
||||||
|
pub token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq)]
|
||||||
|
pub struct Props {
|
||||||
|
pub close: Callback<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
pub fn SignUp() -> Html {
|
pub fn SignUp(props: &Props) -> Html {
|
||||||
let state = use_state(|| Data::default());
|
let state = use_state(|| Data::default());
|
||||||
|
let session = use_context::<Session>().unwrap_or_default();
|
||||||
|
|
||||||
let cloned_state = state.clone();
|
let cloned_state = state.clone();
|
||||||
let on_username_changed = Callback::from(move |event: Event| {
|
let on_username_changed = Callback::from(move |event: Event| {
|
||||||
|
@ -54,35 +81,130 @@ pub fn SignUp() -> Html {
|
||||||
});
|
});
|
||||||
|
|
||||||
let cloned_state = state.clone();
|
let cloned_state = state.clone();
|
||||||
let onsubmit = Callback::from(move |event: SubmitEvent| {
|
let on_password_reenter_changed = Callback::from(move |event: Event| {
|
||||||
event.prevent_default();
|
let password_reenter = event
|
||||||
|
.target()
|
||||||
|
.expect("WTF")
|
||||||
|
.unchecked_into::<HtmlInputElement>()
|
||||||
|
.value();
|
||||||
|
|
||||||
let data = cloned_state.deref().clone();
|
let mut data = cloned_state.deref().clone();
|
||||||
gloo::console::log!("username: ", data.username);
|
data.password_reenter = password_reenter;
|
||||||
gloo::console::log!("email: ", data.email);
|
cloned_state.set(data);
|
||||||
gloo::console::log!("password: ", data.password);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
html! {
|
let cloned_state = state.clone();
|
||||||
|
let on_token_changed = Callback::from(move |event: Event| {
|
||||||
|
let token = event
|
||||||
|
.target()
|
||||||
|
.expect("WTF")
|
||||||
|
.unchecked_into::<HtmlInputElement>()
|
||||||
|
.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! {
|
||||||
<form class="auth-container" onsubmit={onsubmit}>
|
<form class="auth-container" onsubmit={onsubmit}>
|
||||||
<div class="auth-field">
|
<div class="auth-field">
|
||||||
<label for="username">{"Username"}</label><br/>
|
<label for="username">{"Username"}</label><br/>
|
||||||
<input class="auth-input" type="text" name="username" onchange={on_username_changed}/><br/>
|
<input class="auth-input" type="text" name="username" onchange={on_username_changed}/><br/>
|
||||||
</div>
|
</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">
|
<div class="auth-field">
|
||||||
<label for="password">{"Password"}</label><br/>
|
<label for="password">{"Password"}</label><br/>
|
||||||
<input class="auth-input" type="password" name="password" onchange={on_password_changed}/><br/>
|
<input class="auth-input" type="password" name="password" onchange={on_password_changed}/><br/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="auth-field">
|
<div class="auth-field">
|
||||||
<label for="email">{"Email"}</label><br/>
|
<label for="password-reenter">{"Re Enter Password"}</label><br/>
|
||||||
<input class="auth-input" type="text" name="email" onchange={on_email_changed}/><br/>
|
<input class="auth-input" type="password" name="password-reenter" onchange={on_password_reenter_changed}/><br/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
if !state.submitted {
|
||||||
<div class="center-x">
|
<div class="center-x">
|
||||||
<button class="auth-submit" type="submit">{"Sign In"}</button>
|
<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>
|
</div>
|
||||||
</form>
|
</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