feat(account): upgrade external to internal
This commit is contained in:
@@ -79,6 +79,9 @@ pub enum UserError {
|
||||
ValidationError(String),
|
||||
/// Authentication error
|
||||
AuthenticationError(String),
|
||||
/// Upgrade path: the user is already internal — cannot re-upgrade.
|
||||
/// Surfaced by the service as `error_type = "AlreadyInternal"`.
|
||||
AlreadyInternal,
|
||||
}
|
||||
|
||||
impl Display for UserError {
|
||||
@@ -88,6 +91,7 @@ impl Display for UserError {
|
||||
UserError::InvalidPassword(msg) => write!(f, "Invalid password: {}", msg),
|
||||
UserError::ValidationError(msg) => write!(f, "Validation error: {}", msg),
|
||||
UserError::AuthenticationError(msg) => write!(f, "Authentication error: {}", msg),
|
||||
UserError::AlreadyInternal => write!(f, "User is already an internal account"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,6 +512,47 @@ impl User {
|
||||
}
|
||||
}
|
||||
|
||||
/// Promote a currently-external user to an internal account.
|
||||
/// Atomically flips the invariant-linked fields:
|
||||
/// * `is_external` → false
|
||||
/// * `password_hash` → provided (Some) or preserved (None)
|
||||
/// * `storage_quota_bytes` → quota (external users had 0; DB CHECK
|
||||
/// `users_external_no_storage` enforces the pair before this call
|
||||
/// and would refuse a non-zero quota on an external row — the
|
||||
/// write MUST flip `is_external` first, which happens
|
||||
/// transactionally at persist time via the sqlx UPDATE).
|
||||
///
|
||||
/// Password is `Option<String>` because the service allows password-
|
||||
/// less upgrades when magic-link login is available on the
|
||||
/// deployment. When `None`, `password_hash` stays as it was (either
|
||||
/// NULL, or a hash left over from an admin-created invitation —
|
||||
/// externals don't authenticate with it either way).
|
||||
///
|
||||
/// Refuses if the caller is already internal — the upgrade path
|
||||
/// only makes sense on `is_external = true` users. Service pre-
|
||||
/// checks `user.is_external()` before calling; this guard is
|
||||
/// belt-and-braces against a race.
|
||||
///
|
||||
/// Admin combo is impossible by construction: external + admin was
|
||||
/// refused at creation (see `User::new`), so a promoted external
|
||||
/// user always retains their `UserRole::User` — role isn't changed.
|
||||
pub fn promote_to_internal(
|
||||
&mut self,
|
||||
password_hash: Option<String>,
|
||||
storage_quota_bytes: i64,
|
||||
) -> UserResult<()> {
|
||||
if !self.is_external {
|
||||
return Err(UserError::AlreadyInternal);
|
||||
}
|
||||
self.is_external = false;
|
||||
if let Some(hash) = password_hash {
|
||||
self.password_hash = Some(hash);
|
||||
}
|
||||
self.storage_quota_bytes = storage_quota_bytes;
|
||||
self.updated_at = Utc::now();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_image(&mut self, image: Option<String>) {
|
||||
self.image = image;
|
||||
self.updated_at = Utc::now();
|
||||
|
||||
Reference in New Issue
Block a user