account type is permanent and password registration is now working at checkout
Some checks failed
CI / Check Style (push) Has been cancelled
CI / Run Clippy (push) Has been cancelled
CI / Run Tests (push) Has been cancelled

This commit is contained in:
Priec
2026-06-18 22:10:17 +02:00
parent 46cc2459bd
commit f3daa27ce7
24 changed files with 483 additions and 103 deletions

View File

@@ -13,7 +13,6 @@ pub struct Model {
pub id: i32,
#[sea_orm(unique)]
pub user_id: i32,
pub account_type: String,
pub company_name: Option<String>,
pub company_id: Option<String>,
pub tax_id: Option<String>,

View File

@@ -18,6 +18,7 @@ pub struct Model {
pub status: String,
pub total_cents: i64,
pub currency: String,
pub user_id: Option<i32>,
pub account_type: String,
pub company_name: Option<String>,
pub company_id: Option<String>,

View File

@@ -25,6 +25,7 @@ pub struct Model {
pub magic_link_token: Option<String>,
pub magic_link_expiration: Option<DateTimeWithTimeZone>,
pub theme: String,
pub account_type: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@@ -9,11 +9,10 @@ use sea_orm::{ActiveValue, IntoActiveModel, QueryFilter, TryIntoModel};
pub type CustomerProfiles = Entity;
/// The editable profile fields, shared by the profile page and the checkout
/// "save my address" path. `account_type` is "personal" or "company"; the
/// `company_*` fields are only meaningful for company accounts.
/// "save my address" path. The `company_*` fields are only meaningful for
/// company accounts (account type now lives on `users`, fixed at registration).
#[derive(Debug, Default, Clone)]
pub struct ProfileFields {
pub account_type: String,
pub company_name: Option<String>,
pub company_id: Option<String>,
pub tax_id: Option<String>,
@@ -59,7 +58,6 @@ impl Model {
..Default::default()
},
};
active.account_type = ActiveValue::set(fields.account_type);
active.company_name = ActiveValue::set(fields.company_name);
active.company_id = ActiveValue::set(fields.company_id);
active.tax_id = ActiveValue::set(fields.tax_id);

View File

@@ -14,6 +14,9 @@ pub struct Checkout {
pub email: String,
pub phone: String,
pub customer_name: Option<String>,
/// The account that owns this order, if any (a logged-in buyer or a guest
/// who created an account during checkout). `None` for pure guest orders.
pub user_id: Option<i32>,
pub account_type: String,
pub company_name: Option<String>,
pub company_id: Option<String>,
@@ -75,6 +78,7 @@ pub async fn place(ctx: &AppContext, items: &[(i32, i32)], details: Checkout) ->
status: Set("pending".to_string()),
total_cents: Set(subtotal + details.method.price_cents),
currency: Set(currency),
user_id: Set(details.user_id),
account_type: Set(details.account_type),
company_name: Set(details.company_name),
company_id: Set(details.company_id),

View File

@@ -24,6 +24,21 @@ pub struct RegisterParams {
pub email: String,
pub password: String,
pub name: String,
/// "personal" or "company"; permanent for the account. Optional on the wire
/// (older/JSON callers omit it) and normalized via [`normalize_account_type`].
#[serde(default)]
pub account_type: Option<String>,
}
/// Normalize an account type to one of the two permanent values, defaulting to
/// "personal" for anything missing or unexpected. An account's type is chosen
/// once at registration and never changes.
#[must_use]
pub fn normalize_account_type(value: Option<&str>) -> String {
match value.map(str::trim) {
Some("company") => "company".to_string(),
_ => "personal".to_string(),
}
}
#[derive(Debug, Validate, Deserialize)]
@@ -216,6 +231,13 @@ impl Model {
hash::verify_password(password, &self.password)
}
/// Whether this is a company account (vs a personal one). Fixed at
/// registration.
#[must_use]
pub fn is_company(&self) -> bool {
self.account_type == "company"
}
/// Asynchronously creates a user with a password and saves it to the
/// database.
///
@@ -247,6 +269,7 @@ impl Model {
email: ActiveValue::set(params.email.to_string()),
password: ActiveValue::set(password_hash),
name: ActiveValue::set(params.name.to_string()),
account_type: ActiveValue::set(normalize_account_type(params.account_type.as_deref())),
..Default::default()
}
.insert(&txn)
@@ -257,6 +280,41 @@ impl Model {
Ok(user)
}
/// Creates an account on behalf of a checkout guest. The user never picks a
/// password here (a strong random one satisfies the NOT NULL column, as in
/// the OAuth path); they receive a "set your password" link by email. Errors
/// with [`ModelError::EntityAlreadyExists`] if the email is already taken.
///
/// # Errors
///
/// When the email already exists or the insert fails.
pub async fn create_guest_account(
db: &DatabaseConnection,
email: &str,
name: &str,
account_type: &str,
) -> ModelResult<Self> {
let password = PasswordGenerator::new()
.length(16)
.numbers(true)
.lowercase_letters(true)
.uppercase_letters(true)
.symbols(true)
.strict(true)
.generate_one()
.map_err(|e| ModelError::Any(e.into()))?;
Self::create_with_password(
db,
&RegisterParams {
email: email.to_string(),
password,
name: name.to_string(),
account_type: Some(account_type.to_string()),
},
)
.await
}
/// Creates a JWT
///
/// # Errors