nodedb 0.0.0-beta.1

Local-first, real-time, edge-to-cloud hybrid database for multi-modal workloads
Documentation
use crate::types::TenantId;

use super::super::catalog::StoredUser;
use super::super::identity::Role;

/// A stored user record (in-memory cache).
#[derive(Debug, Clone)]
pub struct UserRecord {
    pub user_id: u64,
    pub username: String,
    pub tenant_id: TenantId,
    /// Argon2id password hash (PHC string format).
    pub password_hash: String,
    /// Salt used for SCRAM-SHA-256 (16 bytes).
    pub scram_salt: Vec<u8>,
    /// SCRAM-SHA-256 salted password (for pgwire auth).
    pub scram_salted_password: Vec<u8>,
    pub roles: Vec<Role>,
    pub is_superuser: bool,
    pub is_active: bool,
    /// True if this is a service account (no password, API key auth only).
    pub is_service_account: bool,
    /// Unix timestamp (seconds) when the user was created.
    pub created_at: u64,
    /// Unix timestamp (seconds) when the user was last modified.
    pub updated_at: u64,
    /// Unix timestamp (seconds) when the password expires. 0 = no expiry.
    pub password_expires_at: u64,
    /// MD5 hash for pgwire MD5 auth: `md5(password + username)` as hex string.
    /// Empty for service accounts.
    pub md5_hash: String,
}

impl UserRecord {
    pub(super) fn to_stored(&self) -> StoredUser {
        StoredUser {
            user_id: self.user_id,
            username: self.username.clone(),
            tenant_id: self.tenant_id.as_u32(),
            password_hash: self.password_hash.clone(),
            scram_salt: self.scram_salt.clone(),
            scram_salted_password: self.scram_salted_password.clone(),
            roles: self.roles.iter().map(|r| r.to_string()).collect(),
            is_superuser: self.is_superuser,
            is_active: self.is_active,
            is_service_account: self.is_service_account,
            created_at: self.created_at,
            updated_at: self.updated_at,
            password_expires_at: self.password_expires_at,
            md5_hash: self.md5_hash.clone(),
        }
    }

    pub(super) fn from_stored(s: StoredUser) -> Self {
        let roles: Vec<Role> = s
            .roles
            .iter()
            .map(|r| r.parse().unwrap_or(Role::ReadOnly))
            .collect();
        Self {
            user_id: s.user_id,
            username: s.username,
            tenant_id: TenantId::new(s.tenant_id),
            password_hash: s.password_hash,
            scram_salt: s.scram_salt,
            scram_salted_password: s.scram_salted_password,
            is_superuser: s.is_superuser,
            is_active: s.is_active,
            is_service_account: s.is_service_account,
            created_at: s.created_at,
            updated_at: s.updated_at,
            password_expires_at: s.password_expires_at,
            md5_hash: s.md5_hash,
            roles,
        }
    }
}