//! Customer account area. Currently just the shipping/contact profile, whose //! fields prefill the checkout form. Gated to authenticated non-admin users: //! anonymous visitors are bounced to `/login`. Admins have their own area and //! are sent to the dashboard. use axum_extra::extract::cookie::CookieJar; use loco_rs::prelude::*; use serde::Deserialize; use serde_json::json; use crate::{ controllers::i18n::current_lang, models::customer_profiles::{self, ProfileFields}, shared::guard, }; #[derive(Debug, Deserialize)] struct ProfileForm { account_type: Option, company_name: Option, company_id: Option, tax_id: Option, vat_id: Option, phone_prefix: Option, phone: Option, address: Option, city: Option, zip: Option, country: Option, } fn trimmed(value: Option<&str>) -> Option { value.map(str::trim).filter(|v| !v.is_empty()).map(String::from) } /// Normalize an account type to one of the two known values, defaulting to /// "personal" for anything unexpected. pub fn normalize_account_type(value: Option<&str>) -> String { match value.map(str::trim) { Some("company") => "company".to_string(), _ => "personal".to_string(), } } impl From for ProfileFields { fn from(form: ProfileForm) -> Self { let is_company = normalize_account_type(form.account_type.as_deref()) == "company"; // Company identifiers are only stored for company accounts, so switching // back to personal clears stale data. let company = |v: Option<&str>| if is_company { trimmed(v) } else { None }; Self { account_type: normalize_account_type(form.account_type.as_deref()), company_name: company(form.company_name.as_deref()), company_id: company(form.company_id.as_deref()), tax_id: company(form.tax_id.as_deref()), vat_id: company(form.vat_id.as_deref()), phone_prefix: trimmed(form.phone_prefix.as_deref()), phone: trimmed(form.phone.as_deref()), address: trimmed(form.address.as_deref()), city: trimmed(form.city.as_deref()), zip: trimmed(form.zip.as_deref()), country: trimmed(form.country.as_deref()), } } } /// Render the profile form for `profile` (which may be `None` for a customer /// who hasn't saved anything yet). `saved` shows the success banner after a /// POST. fn profile_view( v: &TeraView, jar: &CookieJar, name: &str, email: &str, profile: Option<&customer_profiles::Model>, saved: bool, ) -> Result { format::view( v, "account/profile.html", json!({ "logged_in_admin": false, "logged_in_customer": true, "saved": saved, "name": name, "email": email, "account_type": profile.map_or("personal", |p| p.account_type.as_str()), "company_name": profile.and_then(|p| p.company_name.clone()), "company_id": profile.and_then(|p| p.company_id.clone()), "tax_id": profile.and_then(|p| p.tax_id.clone()), "vat_id": profile.and_then(|p| p.vat_id.clone()), "phone_prefix": profile.and_then(|p| p.phone_prefix.clone()), "phone": profile.and_then(|p| p.phone.clone()), "address": profile.and_then(|p| p.address.clone()), "city": profile.and_then(|p| p.city.clone()), "zip": profile.and_then(|p| p.zip.clone()), "country": profile.and_then(|p| p.country.clone()), "lang": current_lang(jar), }), ) } #[debug_handler] async fn profile_page( jar: CookieJar, ViewEngine(v): ViewEngine, State(ctx): State, ) -> Result { let Some(user) = guard::current_user(&ctx, &jar).await else { return format::redirect("/login"); }; if guard::is_admin(&ctx, &user) { return format::redirect("/admin/dashboard"); } let profile = customer_profiles::Model::find_for_user(&ctx.db, user.id).await?; profile_view(&v, &jar, &user.name, &user.email, profile.as_ref(), false) } #[debug_handler] async fn save_profile( jar: CookieJar, ViewEngine(v): ViewEngine, State(ctx): State, Form(form): Form, ) -> Result { let Some(user) = guard::current_user(&ctx, &jar).await else { return format::redirect("/login"); }; if guard::is_admin(&ctx, &user) { return format::redirect("/admin/dashboard"); } let profile = customer_profiles::Model::upsert(&ctx.db, user.id, form.into()).await?; profile_view(&v, &jar, &user.name, &user.email, Some(&profile), true) } pub fn routes() -> Routes { Routes::new() .add("/account/profile", get(profile_page)) .add("/account/profile", post(save_profile)) }