diff --git a/.gitignore b/.gitignore index 5c3ec57..aab5642 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ config.yaml flake.nix -/target +target .aider .aider.chat.history.md .aider.input.history .aider.tags.cache.v3 .aider.tags.cache.v3/cache.db data diff --git a/Cargo.lock b/Cargo.lock index d7009fd..dca4e7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -952,6 +952,27 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "chrono-tz" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c6ac4f2c0bf0f44e9161aec9675e1050aa4a530663c4a9e37e108fa948bca9f" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94fea34d77a245229e7746bd2beb786cd2a896f306ff491fb8cecb3074b10a7" +dependencies = [ + "parse-zoneinfo", + "phf_codegen", +] + [[package]] name = "chumsky" version = "0.9.3" @@ -5894,6 +5915,7 @@ dependencies = [ "axum_session_surreal", "axum_typed_multipart", "chrono", + "chrono-tz", "config", "futures", "lettre", diff --git a/Cargo.toml b/Cargo.toml index 6d33bd7..dd4ca83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ axum_session_auth = "0.14.1" axum_session_surreal = "0.2.1" axum_typed_multipart = "0.12.1" chrono = { version = "0.4.39", features = ["serde"] } +chrono-tz = "0.10.1" config = "0.15.4" futures = "0.3.31" lettre = { version = "0.11.11", features = ["rustls-tls"] } diff --git a/src/bin/server.rs b/src/bin/server.rs index 62557ee..3f375f5 100644 --- a/src/bin/server.rs +++ b/src/bin/server.rs @@ -1,7 +1,7 @@ use axum::{ extract::DefaultBodyLimit, middleware::from_fn_with_state, - routing::{delete, get, post}, + routing::{delete, get, patch, post}, Router, }; use axum_session::{SessionConfig, SessionLayer, SessionStore}; @@ -27,7 +27,7 @@ use zettle_db::{ queue_length::queue_length_handler, }, html::{ - account::{delete_account, set_api_key, show_account_page}, + account::{delete_account, set_api_key, show_account_page, update_timezone}, admin_panel::show_admin_panel, documentation::index::show_documentation_index, gdpr::{accept_gdpr, deny_gdpr}, @@ -173,6 +173,7 @@ fn html_routes( .route("/account", get(show_account_page)) .route("/admin", get(show_admin_panel)) .route("/set-api-key", post(set_api_key)) + .route("/update-timezone", patch(update_timezone)) .route("/delete-account", delete(delete_account)) .route( "/signup", diff --git a/src/server/routes/html/account.rs b/src/server/routes/html/account.rs index 4907806..7ed09ee 100644 --- a/src/server/routes/html/account.rs +++ b/src/server/routes/html/account.rs @@ -2,10 +2,12 @@ use axum::{ extract::State, http::{StatusCode, Uri}, response::{IntoResponse, Redirect}, + Form, }; use axum_htmx::HxRedirect; use axum_session_auth::AuthSession; use axum_session_surreal::SessionSurrealPool; +use chrono_tz::TZ_VARIANTS; use surrealdb::{engine::any::Any, Surreal}; use crate::{ @@ -18,7 +20,8 @@ use crate::{ use super::render_block; page_data!(AccountData, "auth/account_settings.html", { - user: User + user: User, + timezones: Vec }); pub async fn show_account_page( @@ -31,9 +34,11 @@ pub async fn show_account_page( None => return Ok(Redirect::to("/").into_response()), }; + let timezones = TZ_VARIANTS.iter().map(|tz| tz.to_string()).collect(); + let output = render_template( AccountData::template_name(), - AccountData { user }, + AccountData { user, timezones }, state.templates.clone(), )?; @@ -67,7 +72,10 @@ pub async fn set_api_key( let output = render_block( AccountData::template_name(), "api_key_section", - AccountData { user: updated_user }, + AccountData { + user: updated_user, + timezones: vec![], + }, state.templates.clone(), )?; @@ -94,3 +102,46 @@ pub async fn delete_account( Ok((HxRedirect::from(Uri::from_static("/")), StatusCode::OK).into_response()) } + +#[derive(Deserialize)] +pub struct UpdateTimezoneForm { + timezone: String, +} + +pub async fn update_timezone( + State(state): State, + auth: AuthSession, Surreal>, + Form(form): Form, +) -> Result { + let user = match &auth.current_user { + Some(user) => user, + None => return Ok(Redirect::to("/").into_response()), + }; + + User::update_timezone(&user.id, &form.timezone, &state.surreal_db_client) + .await + .map_err(|e| HtmlError::new(e, state.templates.clone()))?; + + auth.cache_clear_user(user.id.to_string()); + + // Update the user's API key + let updated_user = User { + timezone: form.timezone, + ..user.clone() + }; + + let timezones = TZ_VARIANTS.iter().map(|tz| tz.to_string()).collect(); + + // Render the API key section block + let output = render_block( + AccountData::template_name(), + "timezone_section", + AccountData { + user: updated_user, + timezones, + }, + state.templates.clone(), + )?; + + Ok(output.into_response()) +} diff --git a/src/server/routes/html/signup.rs b/src/server/routes/html/signup.rs index 3aad748..f5abfa4 100644 --- a/src/server/routes/html/signup.rs +++ b/src/server/routes/html/signup.rs @@ -18,6 +18,7 @@ use super::{render_block, render_template}; pub struct SignupParams { pub email: String, pub password: String, + pub timezone: String, } #[derive(Serialize)] @@ -55,7 +56,14 @@ pub async fn process_signup_and_show_verification( auth: AuthSession, Surreal>, Form(form): Form, ) -> Result { - let user = match User::create_new(form.email, form.password, &state.surreal_db_client).await { + let user = match User::create_new( + form.email, + form.password, + &state.surreal_db_client, + form.timezone, + ) + .await + { Ok(user) => user, Err(e) => { tracing::error!("{:?}", e); diff --git a/src/storage/types/user.rs b/src/storage/types/user.rs index e6c3f40..f384f3e 100644 --- a/src/storage/types/user.rs +++ b/src/storage/types/user.rs @@ -16,7 +16,9 @@ stored_object!(User, "user", { password: String, anonymous: bool, api_key: Option, - admin: bool + admin: bool, + #[serde(default)] + timezone: String }); #[async_trait] @@ -44,6 +46,7 @@ impl User { email: String, password: String, db: &SurrealDbClient, + timezone: String, ) -> Result { // verify that the application allows new creations let systemsettings = SystemSettings::get_current(db).await?; @@ -64,7 +67,8 @@ impl User { admin = $count < 1, // Changed from == 0 to < 1 anonymous = false, created_at = $created_at, - updated_at = $updated_at", + updated_at = $updated_at, + timezone = $timezone", ) .bind(("table", "user")) .bind(("id", id)) @@ -72,6 +76,7 @@ impl User { .bind(("password", password)) .bind(("created_at", now)) .bind(("updated_at", now)) + .bind(("timezone", timezone)) .await? .take(1)?; @@ -214,4 +219,16 @@ impl User { Ok(items) } + pub async fn update_timezone( + user_id: &str, + timezone: &str, + db: &Surreal, + ) -> Result<(), AppError> { + db.query("UPDATE type::thing('user', $user_id) SET timezone = $timezone") + .bind(("table_name", User::table_name())) + .bind(("user_id", user_id.to_string())) + .bind(("timezone", timezone.to_string())) + .await?; + Ok(()) + } } diff --git a/templates/auth/account_settings.html b/templates/auth/account_settings.html index efe1700..cab02fa 100644 --- a/templates/auth/account_settings.html +++ b/templates/auth/account_settings.html @@ -14,6 +14,7 @@ +
+ +
+ + {% block timezone_section %} + + {% endblock %} +
+
+
OR
@@ -52,4 +53,9 @@ Sign in + {% endblock %} \ No newline at end of file