141 lines
4.9 KiB
Rust
141 lines
4.9 KiB
Rust
//! 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<String>,
|
|
company_name: Option<String>,
|
|
company_id: Option<String>,
|
|
tax_id: Option<String>,
|
|
vat_id: Option<String>,
|
|
phone_prefix: Option<String>,
|
|
phone: Option<String>,
|
|
address: Option<String>,
|
|
city: Option<String>,
|
|
zip: Option<String>,
|
|
country: Option<String>,
|
|
}
|
|
|
|
fn trimmed(value: Option<&str>) -> Option<String> {
|
|
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<ProfileForm> 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<Response> {
|
|
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<TeraView>,
|
|
State(ctx): State<AppContext>,
|
|
) -> Result<Response> {
|
|
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<TeraView>,
|
|
State(ctx): State<AppContext>,
|
|
Form(form): Form<ProfileForm>,
|
|
) -> Result<Response> {
|
|
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))
|
|
}
|