Refactor authentication from database to direct CalDAV authentication
Major architectural change to simplify authentication by authenticating directly against CalDAV servers instead of maintaining a local user database. Backend changes: - Remove SQLite database dependencies and user storage - Refactor AuthService to authenticate directly against CalDAV servers - Update JWT tokens to store CalDAV server info instead of user IDs - Implement proper CalDAV calendar discovery with XML parsing - Fix URL construction for CalDAV REPORT requests - Add comprehensive debug logging for authentication flow Frontend changes: - Add server URL input field to login form - Remove registration functionality entirely - Update calendar service to pass CalDAV passwords via headers - Store CalDAV credentials in localStorage for API calls Key improvements: - Simplified architecture eliminates database complexity - Direct CalDAV authentication ensures credentials always work - Proper calendar discovery automatically finds user calendars - Robust error handling and debug logging for troubleshooting 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
38
src/app.rs
38
src/app.rs
@@ -1,7 +1,7 @@
|
||||
use yew::prelude::*;
|
||||
use yew_router::prelude::*;
|
||||
use gloo_storage::{LocalStorage, Storage};
|
||||
use crate::components::{Login, Register, Calendar};
|
||||
use crate::components::{Login, Calendar};
|
||||
use crate::services::{CalendarService, CalendarEvent};
|
||||
use std::collections::HashMap;
|
||||
use chrono::{Local, NaiveDate, Datelike};
|
||||
@@ -12,8 +12,6 @@ enum Route {
|
||||
Home,
|
||||
#[at("/login")]
|
||||
Login,
|
||||
#[at("/register")]
|
||||
Register,
|
||||
#[at("/calendar")]
|
||||
Calendar,
|
||||
}
|
||||
@@ -56,7 +54,6 @@ pub fn App() -> Html {
|
||||
html! {
|
||||
<nav>
|
||||
<Link<Route> to={Route::Login}>{"Login"}</Link<Route>>
|
||||
<Link<Route> to={Route::Register}>{"Register"}</Link<Route>>
|
||||
</nav>
|
||||
}
|
||||
}
|
||||
@@ -83,13 +80,6 @@ pub fn App() -> Html {
|
||||
html! { <Login {on_login} /> }
|
||||
}
|
||||
}
|
||||
Route::Register => {
|
||||
if auth_token.is_some() {
|
||||
html! { <Redirect<Route> to={Route::Calendar}/> }
|
||||
} else {
|
||||
html! { <Register on_register={on_login.clone()} /> }
|
||||
}
|
||||
}
|
||||
Route::Calendar => {
|
||||
if auth_token.is_some() {
|
||||
html! { <CalendarView /> }
|
||||
@@ -136,7 +126,18 @@ fn CalendarView() -> Html {
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let calendar_service = CalendarService::new();
|
||||
|
||||
match calendar_service.refresh_event(&token, &uid).await {
|
||||
// Get password from stored credentials
|
||||
let password = if let Ok(credentials_str) = LocalStorage::get::<String>("caldav_credentials") {
|
||||
if let Ok(credentials) = serde_json::from_str::<serde_json::Value>(&credentials_str) {
|
||||
credentials["password"].as_str().unwrap_or("").to_string()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
match calendar_service.refresh_event(&token, &password, &uid).await {
|
||||
Ok(Some(refreshed_event)) => {
|
||||
// If this is a recurring event, we need to regenerate all occurrences
|
||||
let mut updated_events = (*events).clone();
|
||||
@@ -203,7 +204,18 @@ fn CalendarView() -> Html {
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let calendar_service = CalendarService::new();
|
||||
|
||||
match calendar_service.fetch_events_for_month(&token, current_year, current_month).await {
|
||||
// Get password from stored credentials
|
||||
let password = if let Ok(credentials_str) = LocalStorage::get::<String>("caldav_credentials") {
|
||||
if let Ok(credentials) = serde_json::from_str::<serde_json::Value>(&credentials_str) {
|
||||
credentials["password"].as_str().unwrap_or("").to_string()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
match calendar_service.fetch_events_for_month(&token, &password, current_year, current_month).await {
|
||||
Ok(calendar_events) => {
|
||||
let grouped_events = CalendarService::group_events_by_date(calendar_events);
|
||||
events.set(grouped_events);
|
||||
|
||||
33
src/auth.rs
33
src/auth.rs
@@ -4,29 +4,9 @@ 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 struct CalDAVLoginRequest {
|
||||
pub server_url: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
@@ -34,7 +14,8 @@ pub struct LoginRequest {
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct AuthResponse {
|
||||
pub token: String,
|
||||
pub user: UserInfo,
|
||||
pub username: String,
|
||||
pub server_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -57,11 +38,7 @@ impl AuthService {
|
||||
Self { base_url }
|
||||
}
|
||||
|
||||
pub async fn register(&self, request: RegisterRequest) -> Result<AuthResponse, String> {
|
||||
self.post_json("/auth/register", &request).await
|
||||
}
|
||||
|
||||
pub async fn login(&self, request: LoginRequest) -> Result<AuthResponse, String> {
|
||||
pub async fn login(&self, request: CalDAVLoginRequest) -> Result<AuthResponse, String> {
|
||||
self.post_json("/auth/login", &request).await
|
||||
}
|
||||
|
||||
|
||||
@@ -9,14 +9,24 @@ pub struct LoginProps {
|
||||
|
||||
#[function_component]
|
||||
pub fn Login(props: &LoginProps) -> Html {
|
||||
let server_url = use_state(String::new);
|
||||
let username = use_state(String::new);
|
||||
let password = use_state(String::new);
|
||||
let error_message = use_state(|| Option::<String>::None);
|
||||
let is_loading = 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();
|
||||
Callback::from(move |e: Event| {
|
||||
let target = e.target_unchecked_into::<HtmlInputElement>();
|
||||
server_url.set(target.value());
|
||||
})
|
||||
};
|
||||
|
||||
let on_username_change = {
|
||||
let username = username.clone();
|
||||
Callback::from(move |e: Event| {
|
||||
@@ -34,6 +44,7 @@ pub fn Login(props: &LoginProps) -> Html {
|
||||
};
|
||||
|
||||
let on_submit = {
|
||||
let server_url = server_url.clone();
|
||||
let username = username.clone();
|
||||
let password = password.clone();
|
||||
let error_message = error_message.clone();
|
||||
@@ -43,6 +54,7 @@ pub fn Login(props: &LoginProps) -> Html {
|
||||
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();
|
||||
@@ -50,7 +62,7 @@ pub fn Login(props: &LoginProps) -> Html {
|
||||
let on_login = on_login.clone();
|
||||
|
||||
// Basic client-side validation
|
||||
if username.trim().is_empty() || password.is_empty() {
|
||||
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;
|
||||
}
|
||||
@@ -59,19 +71,27 @@ pub fn Login(props: &LoginProps) -> Html {
|
||||
error_message.set(None);
|
||||
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
match perform_login(username, password).await {
|
||||
Ok(token) => {
|
||||
// Store token in local storage
|
||||
web_sys::console::log_1(&"🚀 Starting login process...".into());
|
||||
match perform_login(server_url.clone(), username.clone(), password.clone()).await {
|
||||
Ok((token, credentials)) => {
|
||||
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("caldav_credentials", &credentials) {
|
||||
error_message.set(Some("Failed to store credentials".to_string()));
|
||||
is_loading.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
is_loading.set(false);
|
||||
on_login.emit(token);
|
||||
}
|
||||
Err(err) => {
|
||||
web_sys::console::log_1(&format!("❌ Login failed: {}", err).into());
|
||||
error_message.set(Some(err));
|
||||
is_loading.set(false);
|
||||
}
|
||||
@@ -83,8 +103,21 @@ pub fn Login(props: &LoginProps) -> Html {
|
||||
html! {
|
||||
<div class="login-container">
|
||||
<div class="login-form">
|
||||
<h2>{"Sign In"}</h2>
|
||||
<h2>{"Sign In to CalDAV"}</h2>
|
||||
<form onsubmit={on_submit}>
|
||||
<div class="form-group">
|
||||
<label for="server_url">{"CalDAV Server URL"}</label>
|
||||
<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}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="username">{"Username"}</label>
|
||||
<input
|
||||
@@ -131,22 +164,43 @@ pub fn Login(props: &LoginProps) -> Html {
|
||||
</form>
|
||||
|
||||
<div class="auth-links">
|
||||
<p>{"Don't have an account? "}<a href="/register">{"Sign up here"}</a></p>
|
||||
<p>{"Enter your CalDAV server credentials to connect to your calendar"}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform login using the auth service
|
||||
async fn perform_login(username: String, password: String) -> Result<String, String> {
|
||||
use crate::auth::{AuthService, LoginRequest};
|
||||
/// Perform login using the CalDAV auth service
|
||||
async fn perform_login(server_url: String, username: String, password: String) -> Result<(String, String), 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 = LoginRequest { username, password };
|
||||
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) => Ok(response.token),
|
||||
Err(err) => Err(err),
|
||||
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
|
||||
});
|
||||
Ok((response.token, credentials.to_string()))
|
||||
},
|
||||
Err(err) => {
|
||||
web_sys::console::log_1(&format!("❌ Backend error: {}", err).into());
|
||||
Err(err)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
pub mod login;
|
||||
pub mod register;
|
||||
pub mod calendar;
|
||||
pub mod event_modal;
|
||||
|
||||
pub use login::Login;
|
||||
pub use register::Register;
|
||||
pub use calendar::Calendar;
|
||||
pub use event_modal::EventModal;
|
||||
@@ -1,235 +0,0 @@
|
||||
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),
|
||||
}
|
||||
}
|
||||
@@ -139,6 +139,7 @@ impl CalendarService {
|
||||
pub async fn fetch_events_for_month(
|
||||
&self,
|
||||
token: &str,
|
||||
password: &str,
|
||||
year: i32,
|
||||
month: u32
|
||||
) -> Result<Vec<CalendarEvent>, String> {
|
||||
@@ -154,6 +155,9 @@ impl CalendarService {
|
||||
|
||||
request.headers().set("Authorization", &format!("Bearer {}", token))
|
||||
.map_err(|e| format!("Authorization header setting failed: {:?}", e))?;
|
||||
|
||||
request.headers().set("X-CalDAV-Password", password)
|
||||
.map_err(|e| format!("Password header setting failed: {:?}", e))?;
|
||||
|
||||
let resp_value = JsFuture::from(window.fetch_with_request(&request))
|
||||
.await
|
||||
@@ -407,7 +411,7 @@ impl CalendarService {
|
||||
}
|
||||
|
||||
/// Refresh a single event by UID from the CalDAV server
|
||||
pub async fn refresh_event(&self, token: &str, uid: &str) -> Result<Option<CalendarEvent>, String> {
|
||||
pub async fn refresh_event(&self, token: &str, password: &str, uid: &str) -> Result<Option<CalendarEvent>, String> {
|
||||
let window = web_sys::window().ok_or("No global window exists")?;
|
||||
|
||||
let opts = RequestInit::new();
|
||||
@@ -420,6 +424,9 @@ impl CalendarService {
|
||||
|
||||
request.headers().set("Authorization", &format!("Bearer {}", token))
|
||||
.map_err(|e| format!("Authorization header setting failed: {:?}", e))?;
|
||||
|
||||
request.headers().set("X-CalDAV-Password", password)
|
||||
.map_err(|e| format!("Password header setting failed: {:?}", e))?;
|
||||
|
||||
let resp_value = JsFuture::from(window.fetch_with_request(&request))
|
||||
.await
|
||||
|
||||
Reference in New Issue
Block a user