// Frontend authentication module - connects to backend API use serde::{Deserialize, Serialize}; use wasm_bindgen::JsCast; use wasm_bindgen_futures::JsFuture; use web_sys::{Request, RequestInit, RequestMode, Response}; #[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, } #[derive(Debug, Deserialize)] pub struct ApiErrorResponse { pub error: String, pub status: u16, } // Frontend auth service - connects to backend API pub struct AuthService { base_url: String, } impl AuthService { pub fn new() -> Self { // Default to localhost backend - could be configurable via env var in the future Self { base_url: "http://localhost:3000/api".to_string(), } } pub async fn register(&self, request: RegisterRequest) -> Result { self.post_json("/auth/register", &request).await } pub async fn login(&self, request: LoginRequest) -> Result { self.post_json("/auth/login", &request).await } pub async fn verify_token(&self, token: &str) -> Result { let response = self.get_with_auth("/auth/verify", token).await?; let json_value: serde_json::Value = response; if let Some(user_obj) = json_value.get("user") { serde_json::from_value(user_obj.clone()) .map_err(|e| format!("Failed to parse user info: {}", e)) } else { Err("Invalid response format".to_string()) } } // Helper method for POST requests with JSON body async fn post_json Deserialize<'de>>( &self, endpoint: &str, body: &T, ) -> Result { let window = web_sys::window().ok_or("No global window exists")?; let json_body = serde_json::to_string(body) .map_err(|e| format!("JSON serialization failed: {}", e))?; let opts = RequestInit::new(); opts.set_method("POST"); opts.set_mode(RequestMode::Cors); opts.set_body(&wasm_bindgen::JsValue::from_str(&json_body)); let url = format!("{}{}", self.base_url, endpoint); let request = Request::new_with_str_and_init(&url, &opts) .map_err(|e| format!("Request creation failed: {:?}", e))?; request.headers().set("Content-Type", "application/json") .map_err(|e| format!("Header setting failed: {:?}", e))?; let resp_value = JsFuture::from(window.fetch_with_request(&request)) .await .map_err(|e| format!("Network request failed: {:?}", e))?; let resp: Response = resp_value.dyn_into() .map_err(|e| format!("Response cast failed: {:?}", e))?; let text = JsFuture::from(resp.text() .map_err(|e| format!("Text extraction failed: {:?}", e))?) .await .map_err(|e| format!("Text promise failed: {:?}", e))?; let text_string = text.as_string() .ok_or("Response text is not a string")?; if resp.ok() { serde_json::from_str::(&text_string) .map_err(|e| format!("JSON parsing failed: {}", e)) } else { // Try to parse error response if let Ok(error_response) = serde_json::from_str::(&text_string) { Err(error_response.error) } else { Err(format!("Request failed with status {}", resp.status())) } } } // Helper method for GET requests with Authorization header async fn get_with_auth( &self, endpoint: &str, token: &str, ) -> Result { let window = web_sys::window().ok_or("No global window exists")?; let opts = RequestInit::new(); opts.set_method("GET"); opts.set_mode(RequestMode::Cors); let url = format!("{}{}", self.base_url, endpoint); let request = Request::new_with_str_and_init(&url, &opts) .map_err(|e| format!("Request creation failed: {:?}", e))?; request.headers().set("Authorization", &format!("Bearer {}", token)) .map_err(|e| format!("Authorization header setting failed: {:?}", e))?; let resp_value = JsFuture::from(window.fetch_with_request(&request)) .await .map_err(|e| format!("Network request failed: {:?}", e))?; let resp: Response = resp_value.dyn_into() .map_err(|e| format!("Response cast failed: {:?}", e))?; let text = JsFuture::from(resp.text() .map_err(|e| format!("Text extraction failed: {:?}", e))?) .await .map_err(|e| format!("Text promise failed: {:?}", e))?; let text_string = text.as_string() .ok_or("Response text is not a string")?; if resp.ok() { serde_json::from_str::(&text_string) .map_err(|e| format!("JSON parsing failed: {}", e)) } else { // Try to parse error response if let Ok(error_response) = serde_json::from_str::(&text_string) { Err(error_response.error) } else { Err(format!("Request failed with status {}", resp.status())) } } } }