- Add comprehensive authentication module with mock service - Create login and registration components with form validation - Implement protected routing with yew-router - Add responsive UI styling with gradient design - Enable JWT token persistence via localStorage - Support demo credentials (demo/password) and flexible auth for development 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
235 lines
8.2 KiB
Rust
235 lines
8.2 KiB
Rust
use yew::prelude::*;
|
|
use web_sys::HtmlInputElement;
|
|
use gloo_storage::{LocalStorage, Storage};
|
|
|
|
#[derive(Properties, PartialEq)]
|
|
pub struct RegisterProps {
|
|
pub on_register: Callback<String>, // Callback with JWT token
|
|
}
|
|
|
|
#[function_component]
|
|
pub fn Register(props: &RegisterProps) -> Html {
|
|
let username = use_state(String::new);
|
|
let email = use_state(String::new);
|
|
let password = use_state(String::new);
|
|
let confirm_password = use_state(String::new);
|
|
let error_message = use_state(|| Option::<String>::None);
|
|
let is_loading = use_state(|| false);
|
|
|
|
let username_ref = use_node_ref();
|
|
let email_ref = use_node_ref();
|
|
let password_ref = use_node_ref();
|
|
let confirm_password_ref = use_node_ref();
|
|
|
|
let on_username_change = {
|
|
let username = username.clone();
|
|
Callback::from(move |e: Event| {
|
|
let target = e.target_unchecked_into::<HtmlInputElement>();
|
|
username.set(target.value());
|
|
})
|
|
};
|
|
|
|
let on_email_change = {
|
|
let email = email.clone();
|
|
Callback::from(move |e: Event| {
|
|
let target = e.target_unchecked_into::<HtmlInputElement>();
|
|
email.set(target.value());
|
|
})
|
|
};
|
|
|
|
let on_password_change = {
|
|
let password = password.clone();
|
|
Callback::from(move |e: Event| {
|
|
let target = e.target_unchecked_into::<HtmlInputElement>();
|
|
password.set(target.value());
|
|
})
|
|
};
|
|
|
|
let on_confirm_password_change = {
|
|
let confirm_password = confirm_password.clone();
|
|
Callback::from(move |e: Event| {
|
|
let target = e.target_unchecked_into::<HtmlInputElement>();
|
|
confirm_password.set(target.value());
|
|
})
|
|
};
|
|
|
|
let on_submit = {
|
|
let username = username.clone();
|
|
let email = email.clone();
|
|
let password = password.clone();
|
|
let confirm_password = confirm_password.clone();
|
|
let error_message = error_message.clone();
|
|
let is_loading = is_loading.clone();
|
|
let on_register = props.on_register.clone();
|
|
|
|
Callback::from(move |e: SubmitEvent| {
|
|
e.prevent_default();
|
|
|
|
let username = (*username).clone();
|
|
let email = (*email).clone();
|
|
let password = (*password).clone();
|
|
let confirm_password = (*confirm_password).clone();
|
|
let error_message = error_message.clone();
|
|
let is_loading = is_loading.clone();
|
|
let on_register = on_register.clone();
|
|
|
|
// Client-side validation
|
|
if let Err(validation_error) = validate_registration(&username, &email, &password, &confirm_password) {
|
|
error_message.set(Some(validation_error));
|
|
return;
|
|
}
|
|
|
|
is_loading.set(true);
|
|
error_message.set(None);
|
|
|
|
wasm_bindgen_futures::spawn_local(async move {
|
|
match perform_registration(username, email, password).await {
|
|
Ok(token) => {
|
|
// Store token in local storage
|
|
if let Err(_) = LocalStorage::set("auth_token", &token) {
|
|
error_message.set(Some("Failed to store authentication token".to_string()));
|
|
is_loading.set(false);
|
|
return;
|
|
}
|
|
|
|
is_loading.set(false);
|
|
on_register.emit(token);
|
|
}
|
|
Err(err) => {
|
|
error_message.set(Some(err));
|
|
is_loading.set(false);
|
|
}
|
|
}
|
|
});
|
|
})
|
|
};
|
|
|
|
html! {
|
|
<div class="register-container">
|
|
<div class="register-form">
|
|
<h2>{"Create Account"}</h2>
|
|
<form onsubmit={on_submit}>
|
|
<div class="form-group">
|
|
<label for="username">{"Username"}</label>
|
|
<input
|
|
ref={username_ref}
|
|
type="text"
|
|
id="username"
|
|
placeholder="Choose a username"
|
|
value={(*username).clone()}
|
|
onchange={on_username_change}
|
|
disabled={*is_loading}
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="email">{"Email"}</label>
|
|
<input
|
|
ref={email_ref}
|
|
type="email"
|
|
id="email"
|
|
placeholder="Enter your email"
|
|
value={(*email).clone()}
|
|
onchange={on_email_change}
|
|
disabled={*is_loading}
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="password">{"Password"}</label>
|
|
<input
|
|
ref={password_ref}
|
|
type="password"
|
|
id="password"
|
|
placeholder="Choose a password"
|
|
value={(*password).clone()}
|
|
onchange={on_password_change}
|
|
disabled={*is_loading}
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="confirm-password">{"Confirm Password"}</label>
|
|
<input
|
|
ref={confirm_password_ref}
|
|
type="password"
|
|
id="confirm-password"
|
|
placeholder="Confirm your password"
|
|
value={(*confirm_password).clone()}
|
|
onchange={on_confirm_password_change}
|
|
disabled={*is_loading}
|
|
/>
|
|
</div>
|
|
|
|
{
|
|
if let Some(error) = (*error_message).clone() {
|
|
html! { <div class="error-message">{error}</div> }
|
|
} else {
|
|
html! {}
|
|
}
|
|
}
|
|
|
|
<button type="submit" disabled={*is_loading} class="register-button">
|
|
{
|
|
if *is_loading {
|
|
"Creating Account..."
|
|
} else {
|
|
"Create Account"
|
|
}
|
|
}
|
|
</button>
|
|
</form>
|
|
|
|
<div class="auth-links">
|
|
<p>{"Already have an account? "}<a href="/login">{"Sign in here"}</a></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
/// Validate registration form data
|
|
fn validate_registration(username: &str, email: &str, password: &str, confirm_password: &str) -> Result<(), String> {
|
|
if username.trim().is_empty() {
|
|
return Err("Username is required".to_string());
|
|
}
|
|
|
|
if username.len() < 3 {
|
|
return Err("Username must be at least 3 characters long".to_string());
|
|
}
|
|
|
|
if email.trim().is_empty() {
|
|
return Err("Email is required".to_string());
|
|
}
|
|
|
|
if !email.contains('@') {
|
|
return Err("Please enter a valid email address".to_string());
|
|
}
|
|
|
|
if password.is_empty() {
|
|
return Err("Password is required".to_string());
|
|
}
|
|
|
|
if password.len() < 6 {
|
|
return Err("Password must be at least 6 characters long".to_string());
|
|
}
|
|
|
|
if password != confirm_password {
|
|
return Err("Passwords do not match".to_string());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Perform registration using the auth service
|
|
async fn perform_registration(username: String, email: String, password: String) -> Result<String, String> {
|
|
use crate::auth::{AuthService, RegisterRequest};
|
|
|
|
let auth_service = AuthService::new();
|
|
let request = RegisterRequest { username, email, password };
|
|
|
|
match auth_service.register(request).await {
|
|
Ok(response) => Ok(response.token),
|
|
Err(err) => Err(err),
|
|
}
|
|
} |