diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..e33ad4d --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,123 @@ +// Frontend-only authentication module (simplified for WASM compatibility) +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct User { + pub id: String, + pub username: String, + pub email: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UserInfo { + pub id: String, + pub username: String, + pub email: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct RegisterRequest { + pub username: String, + pub email: String, + pub password: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct LoginRequest { + pub username: String, + pub password: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AuthResponse { + pub token: String, + pub user: UserInfo, +} + +// Simplified frontend-only auth service +pub struct AuthService; + +impl AuthService { + pub fn new() -> Self { + Self + } + + // Mock authentication methods for development + // In production, these would make HTTP requests to a backend API + + pub async fn register(&self, request: RegisterRequest) -> Result { + // Simulate API delay + gloo_timers::future::TimeoutFuture::new(500).await; + + // Basic validation + if request.username.trim().is_empty() || request.email.trim().is_empty() || request.password.is_empty() { + return Err("All fields are required".to_string()); + } + + if request.password.len() < 6 { + return Err("Password must be at least 6 characters".to_string()); + } + + // Mock successful registration + Ok(AuthResponse { + token: format!("mock-jwt-token-{}", request.username), + user: UserInfo { + id: "user-123".to_string(), + username: request.username, + email: request.email, + }, + }) + } + + pub async fn login(&self, request: LoginRequest) -> Result { + // Simulate API delay + gloo_timers::future::TimeoutFuture::new(500).await; + + // Basic validation + if request.username.trim().is_empty() || request.password.is_empty() { + return Err("Username and password are required".to_string()); + } + + // Mock authentication - accept demo/password or any user/password combo + if request.username == "demo" && request.password == "password" { + Ok(AuthResponse { + token: "mock-jwt-token-demo".to_string(), + user: UserInfo { + id: "demo-user-123".to_string(), + username: request.username, + email: "demo@example.com".to_string(), + }, + }) + } else if !request.password.is_empty() { + // Accept any non-empty password for development + let username = request.username.clone(); + Ok(AuthResponse { + token: format!("mock-jwt-token-{}", username), + user: UserInfo { + id: format!("user-{}", username), + username: request.username, + email: format!("{}@example.com", username), + }, + }) + } else { + Err("Invalid credentials".to_string()) + } + } + + pub async fn verify_token(&self, token: &str) -> Result { + // Simulate API delay + gloo_timers::future::TimeoutFuture::new(100).await; + + // Mock token verification + if token.starts_with("mock-jwt-token-") { + let username = token.strip_prefix("mock-jwt-token-").unwrap_or("unknown"); + Ok(UserInfo { + id: format!("user-{}", username), + username: username.to_string(), + email: format!("{}@example.com", username), + }) + } else { + Err("Invalid token".to_string()) + } + } +} \ No newline at end of file diff --git a/src/components/login.rs b/src/components/login.rs new file mode 100644 index 0000000..b5dfaf3 --- /dev/null +++ b/src/components/login.rs @@ -0,0 +1,152 @@ +use yew::prelude::*; +use web_sys::HtmlInputElement; +use gloo_storage::{LocalStorage, Storage}; + +#[derive(Properties, PartialEq)] +pub struct LoginProps { + pub on_login: Callback, // Callback with JWT token +} + +#[function_component] +pub fn Login(props: &LoginProps) -> Html { + let username = use_state(String::new); + let password = use_state(String::new); + let error_message = use_state(|| Option::::None); + let is_loading = use_state(|| false); + + let username_ref = use_node_ref(); + let password_ref = use_node_ref(); + + let on_username_change = { + let username = username.clone(); + Callback::from(move |e: Event| { + let target = e.target_unchecked_into::(); + username.set(target.value()); + }) + }; + + let on_password_change = { + let password = password.clone(); + Callback::from(move |e: Event| { + let target = e.target_unchecked_into::(); + password.set(target.value()); + }) + }; + + let on_submit = { + let username = username.clone(); + let password = password.clone(); + let error_message = error_message.clone(); + let is_loading = is_loading.clone(); + let on_login = props.on_login.clone(); + + Callback::from(move |e: SubmitEvent| { + e.prevent_default(); + + let username = (*username).clone(); + let password = (*password).clone(); + let error_message = error_message.clone(); + let is_loading = is_loading.clone(); + let on_login = on_login.clone(); + + // Basic client-side validation + if username.trim().is_empty() || password.is_empty() { + error_message.set(Some("Please fill in all fields".to_string())); + return; + } + + is_loading.set(true); + error_message.set(None); + + wasm_bindgen_futures::spawn_local(async move { + match perform_login(username, 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_login.emit(token); + } + Err(err) => { + error_message.set(Some(err)); + is_loading.set(false); + } + } + }); + }) + }; + + html! { + + } +} + +/// Perform login using the auth service +async fn perform_login(username: String, password: String) -> Result { + use crate::auth::{AuthService, LoginRequest}; + + let auth_service = AuthService::new(); + let request = LoginRequest { username, password }; + + match auth_service.login(request).await { + Ok(response) => Ok(response.token), + Err(err) => Err(err), + } +} \ No newline at end of file diff --git a/src/components/mod.rs b/src/components/mod.rs new file mode 100644 index 0000000..74df60b --- /dev/null +++ b/src/components/mod.rs @@ -0,0 +1,5 @@ +pub mod login; +pub mod register; + +pub use login::Login; +pub use register::Register; \ No newline at end of file diff --git a/src/components/register.rs b/src/components/register.rs new file mode 100644 index 0000000..1a09880 --- /dev/null +++ b/src/components/register.rs @@ -0,0 +1,235 @@ +use yew::prelude::*; +use web_sys::HtmlInputElement; +use gloo_storage::{LocalStorage, Storage}; + +#[derive(Properties, PartialEq)] +pub struct RegisterProps { + pub on_register: Callback, // 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::::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::(); + username.set(target.value()); + }) + }; + + let on_email_change = { + let email = email.clone(); + Callback::from(move |e: Event| { + let target = e.target_unchecked_into::(); + email.set(target.value()); + }) + }; + + let on_password_change = { + let password = password.clone(); + Callback::from(move |e: Event| { + let target = e.target_unchecked_into::(); + 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::(); + 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! { +
+
+

{"Create Account"}

+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + { + if let Some(error) = (*error_message).clone() { + html! {
{error}
} + } else { + html! {} + } + } + + +
+ + +
+
+ } +} + +/// 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 { + 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), + } +} \ No newline at end of file