- Implement show/hide password functionality with eye icon toggle button - Add dynamic input type switching between password and text - Position toggle button inside password input field with proper styling - Include hover, focus states and accessibility features (tabindex, title) - Use FontAwesome eye/eye-slash icons for visual feedback - Maintain secure default (password hidden) with optional visibility - Integrate proper tab order with existing form elements 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
352 lines
14 KiB
Rust
352 lines
14 KiB
Rust
use gloo_storage::{LocalStorage, Storage};
|
|
use web_sys::HtmlInputElement;
|
|
use yew::prelude::*;
|
|
|
|
#[derive(Properties, PartialEq)]
|
|
pub struct LoginProps {
|
|
pub on_login: Callback<String>, // Callback with JWT token
|
|
}
|
|
|
|
#[function_component]
|
|
pub fn Login(props: &LoginProps) -> Html {
|
|
// Load remembered values from LocalStorage on mount
|
|
let server_url = use_state(|| {
|
|
LocalStorage::get::<String>("remembered_server_url").unwrap_or_default()
|
|
});
|
|
let username = use_state(|| {
|
|
LocalStorage::get::<String>("remembered_username").unwrap_or_default()
|
|
});
|
|
let password = use_state(String::new);
|
|
let error_message = use_state(|| Option::<String>::None);
|
|
let is_loading = use_state(|| false);
|
|
|
|
// Remember checkboxes state - default to checked
|
|
let remember_server = use_state(|| true);
|
|
let remember_username = use_state(|| true);
|
|
|
|
// Password visibility toggle
|
|
let show_password = use_state(|| false);
|
|
|
|
let server_url_ref = use_node_ref();
|
|
let username_ref = use_node_ref();
|
|
let password_ref = use_node_ref();
|
|
|
|
let on_server_url_change = {
|
|
let server_url = server_url.clone();
|
|
let remember_server = remember_server.clone();
|
|
Callback::from(move |e: Event| {
|
|
let target = e.target_unchecked_into::<HtmlInputElement>();
|
|
let new_value = target.value();
|
|
server_url.set(new_value.clone());
|
|
|
|
// Save to localStorage immediately if remember is checked
|
|
if *remember_server {
|
|
let _ = LocalStorage::set("remembered_server_url", new_value);
|
|
}
|
|
})
|
|
};
|
|
|
|
let on_username_change = {
|
|
let username = username.clone();
|
|
let remember_username = remember_username.clone();
|
|
Callback::from(move |e: Event| {
|
|
let target = e.target_unchecked_into::<HtmlInputElement>();
|
|
let new_value = target.value();
|
|
username.set(new_value.clone());
|
|
|
|
// Save to localStorage immediately if remember is checked
|
|
if *remember_username {
|
|
let _ = LocalStorage::set("remembered_username", new_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_remember_server_change = {
|
|
let remember_server = remember_server.clone();
|
|
let server_url = server_url.clone();
|
|
Callback::from(move |e: Event| {
|
|
let target = e.target_unchecked_into::<HtmlInputElement>();
|
|
let checked = target.checked();
|
|
remember_server.set(checked);
|
|
|
|
if checked {
|
|
let _ = LocalStorage::set("remembered_server_url", (*server_url).clone());
|
|
} else {
|
|
let _ = LocalStorage::delete("remembered_server_url");
|
|
}
|
|
})
|
|
};
|
|
|
|
let on_remember_username_change = {
|
|
let remember_username = remember_username.clone();
|
|
let username = username.clone();
|
|
Callback::from(move |e: Event| {
|
|
let target = e.target_unchecked_into::<HtmlInputElement>();
|
|
let checked = target.checked();
|
|
remember_username.set(checked);
|
|
|
|
if checked {
|
|
let _ = LocalStorage::set("remembered_username", (*username).clone());
|
|
} else {
|
|
let _ = LocalStorage::delete("remembered_username");
|
|
}
|
|
})
|
|
};
|
|
|
|
let on_toggle_password_visibility = {
|
|
let show_password = show_password.clone();
|
|
Callback::from(move |_| {
|
|
show_password.set(!*show_password);
|
|
})
|
|
};
|
|
|
|
let on_submit = {
|
|
let server_url = server_url.clone();
|
|
let username = username.clone();
|
|
let password = password.clone();
|
|
let error_message = error_message.clone();
|
|
let is_loading = is_loading.clone();
|
|
let remember_server = remember_server.clone();
|
|
let remember_username = remember_username.clone();
|
|
let on_login = props.on_login.clone();
|
|
|
|
Callback::from(move |e: SubmitEvent| {
|
|
e.prevent_default();
|
|
|
|
let server_url = (*server_url).clone();
|
|
let username = (*username).clone();
|
|
let password = (*password).clone();
|
|
let error_message = error_message.clone();
|
|
let is_loading = is_loading.clone();
|
|
let remember_server_value = *remember_server;
|
|
let remember_username_value = *remember_username;
|
|
let on_login = on_login.clone();
|
|
|
|
// Basic client-side validation
|
|
if server_url.trim().is_empty() || 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 {
|
|
web_sys::console::log_1(&"🚀 Starting login process...".into());
|
|
match perform_login(server_url.clone(), username.clone(), password.clone()).await {
|
|
Ok((token, session_token, credentials, preferences)) => {
|
|
web_sys::console::log_1(&"✅ Login successful!".into());
|
|
// Store token and credentials 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;
|
|
}
|
|
if let Err(_) = LocalStorage::set("session_token", &session_token) {
|
|
error_message
|
|
.set(Some("Failed to store session token".to_string()));
|
|
is_loading.set(false);
|
|
return;
|
|
}
|
|
if let Err(_) = LocalStorage::set("caldav_credentials", &credentials) {
|
|
error_message.set(Some("Failed to store credentials".to_string()));
|
|
is_loading.set(false);
|
|
return;
|
|
}
|
|
|
|
// Store preferences from database
|
|
if let Ok(prefs_json) = serde_json::to_string(&preferences) {
|
|
let _ = LocalStorage::set("user_preferences", &prefs_json);
|
|
}
|
|
|
|
// Save server URL and username to LocalStorage if remember checkboxes are checked
|
|
if remember_server_value {
|
|
let _ = LocalStorage::set("remembered_server_url", server_url.clone());
|
|
}
|
|
if remember_username_value {
|
|
let _ = LocalStorage::set("remembered_username", username.clone());
|
|
}
|
|
|
|
is_loading.set(false);
|
|
on_login.emit(token);
|
|
}
|
|
Err(err) => {
|
|
web_sys::console::log_1(&format!("❌ Login failed: {}", err).into());
|
|
// Clear any existing invalid tokens
|
|
let _ = LocalStorage::delete("auth_token");
|
|
let _ = LocalStorage::delete("session_token");
|
|
let _ = LocalStorage::delete("caldav_credentials");
|
|
error_message.set(Some(err));
|
|
is_loading.set(false);
|
|
}
|
|
}
|
|
});
|
|
})
|
|
};
|
|
|
|
html! {
|
|
<div class="login-container">
|
|
<div class="login-form">
|
|
<h2>{"Sign In to CalDAV"}</h2>
|
|
<form onsubmit={on_submit}>
|
|
<div class="form-group">
|
|
<label for="server_url">{"CalDAV Server URL"}</label>
|
|
<div class="input-with-checkbox">
|
|
<input
|
|
ref={server_url_ref}
|
|
type="text"
|
|
id="server_url"
|
|
placeholder="https://your-caldav-server.com/dav/"
|
|
value={(*server_url).clone()}
|
|
onchange={on_server_url_change}
|
|
disabled={*is_loading}
|
|
tabindex="1"
|
|
/>
|
|
<div class="remember-checkbox">
|
|
<label for="remember_server">{"Remember"}</label>
|
|
<input
|
|
type="checkbox"
|
|
id="remember_server"
|
|
checked={*remember_server}
|
|
onchange={on_remember_server_change}
|
|
tabindex="4"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="username">{"Username"}</label>
|
|
<div class="input-with-checkbox">
|
|
<input
|
|
ref={username_ref}
|
|
type="text"
|
|
id="username"
|
|
placeholder="Enter your username"
|
|
value={(*username).clone()}
|
|
onchange={on_username_change}
|
|
disabled={*is_loading}
|
|
tabindex="2"
|
|
/>
|
|
<div class="remember-checkbox">
|
|
<label for="remember_username">{"Remember"}</label>
|
|
<input
|
|
type="checkbox"
|
|
id="remember_username"
|
|
checked={*remember_username}
|
|
onchange={on_remember_username_change}
|
|
tabindex="5"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="password">{"Password"}</label>
|
|
<div class="password-input-container">
|
|
<input
|
|
ref={password_ref}
|
|
type={if *show_password { "text" } else { "password" }}
|
|
id="password"
|
|
placeholder="Enter your password"
|
|
value={(*password).clone()}
|
|
onchange={on_password_change}
|
|
disabled={*is_loading}
|
|
tabindex="3"
|
|
/>
|
|
<button
|
|
type="button"
|
|
class="password-toggle-btn"
|
|
onclick={on_toggle_password_visibility}
|
|
tabindex="6"
|
|
title={if *show_password { "Hide password" } else { "Show password" }}
|
|
>
|
|
<i class={if *show_password { "fas fa-eye-slash" } else { "fas fa-eye" }}></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{
|
|
if let Some(error) = (*error_message).clone() {
|
|
html! { <div class="error-message">{error}</div> }
|
|
} else {
|
|
html! {}
|
|
}
|
|
}
|
|
|
|
<button type="submit" disabled={*is_loading} class="login-button">
|
|
{
|
|
if *is_loading {
|
|
"Signing in..."
|
|
} else {
|
|
"Sign In"
|
|
}
|
|
}
|
|
</button>
|
|
</form>
|
|
|
|
<div class="auth-links">
|
|
<p>{"Enter your CalDAV server credentials to connect to your calendar"}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
/// Perform login using the CalDAV auth service
|
|
async fn perform_login(
|
|
server_url: String,
|
|
username: String,
|
|
password: String,
|
|
) -> Result<(String, String, String, serde_json::Value), String> {
|
|
use crate::auth::{AuthService, CalDAVLoginRequest};
|
|
use serde_json;
|
|
|
|
web_sys::console::log_1(&format!("📡 Creating auth service and request...").into());
|
|
|
|
let auth_service = AuthService::new();
|
|
let request = CalDAVLoginRequest {
|
|
server_url: server_url.clone(),
|
|
username: username.clone(),
|
|
password: password.clone(),
|
|
};
|
|
|
|
web_sys::console::log_1(&format!("🚀 Sending login request to backend...").into());
|
|
|
|
match auth_service.login(request).await {
|
|
Ok(response) => {
|
|
web_sys::console::log_1(&format!("✅ Backend responded successfully").into());
|
|
// Create credentials object to store
|
|
let credentials = serde_json::json!({
|
|
"server_url": server_url,
|
|
"username": username,
|
|
"password": password
|
|
});
|
|
|
|
// Extract preferences as JSON
|
|
let preferences = serde_json::json!({
|
|
"calendar_selected_date": response.preferences.calendar_selected_date,
|
|
"calendar_time_increment": response.preferences.calendar_time_increment,
|
|
"calendar_view_mode": response.preferences.calendar_view_mode,
|
|
"calendar_theme": response.preferences.calendar_theme,
|
|
"calendar_colors": response.preferences.calendar_colors,
|
|
});
|
|
|
|
Ok((response.token, response.session_token, credentials.to_string(), preferences))
|
|
}
|
|
Err(err) => {
|
|
web_sys::console::log_1(&format!("❌ Backend error: {}", err).into());
|
|
Err(err)
|
|
}
|
|
}
|
|
}
|