kcode-k1-users 0.1.0

User facade over K1 accounts and persons
Documentation
use kcode_k1_accounts::{Account, K1Accounts, NewAccount, PersonId};
use kcode_k1_invites::{InviteCode, UserId};
use kcode_k1_persons::{K1Persons, TxId};
use std::sync::{Arc, Mutex, MutexGuard};

pub struct NewUser {
    account: NewAccount,
    full_name: String,
}

impl NewUser {
    pub fn new(
        username: &str,
        full_name: &str,
        public_key: [u8; 32],
        tos_revision: u64,
        tos_digest: [u8; 32],
        acceptance_signature: [u8; 64],
    ) -> Result<Self, String> {
        let account = NewAccount::new(
            username,
            zero_person_id(),
            public_key,
            tos_revision,
            tos_digest,
            acceptance_signature,
        )?;
        validate_full_name(full_name)?;
        Ok(Self {
            account,
            full_name: copy_string(full_name)?,
        })
    }

    pub fn username(&self) -> &str {
        self.account.username()
    }

    pub fn full_name(&self) -> &str {
        &self.full_name
    }

    pub fn public_key(&self) -> [u8; 32] {
        self.account.public_key()
    }

    pub fn tos_revision(&self) -> u64 {
        self.account.tos_revision()
    }

    pub fn tos_digest(&self) -> [u8; 32] {
        self.account.tos_digest()
    }

    pub fn acceptance_signature(&self) -> [u8; 64] {
        self.account.acceptance_signature()
    }
}

pub struct User {
    account: Account,
    person_id: PersonId,
    full_name: String,
}

impl User {
    pub fn user_id(&self) -> UserId {
        self.account.user_id()
    }

    pub fn username(&self) -> &str {
        self.account.username()
    }

    pub fn person_anchor_id(&self) -> PersonId {
        self.account.person_id()
    }

    pub fn person_id(&self) -> PersonId {
        self.person_id
    }

    pub fn full_name(&self) -> &str {
        &self.full_name
    }

    pub fn public_key(&self) -> [u8; 32] {
        self.account.public_key()
    }

    pub fn tos_revision(&self) -> u64 {
        self.account.tos_revision()
    }

    pub fn tos_digest(&self) -> [u8; 32] {
        self.account.tos_digest()
    }

    pub fn acceptance_signature(&self) -> [u8; 64] {
        self.account.acceptance_signature()
    }
}

pub struct K1Users {
    accounts: Arc<K1Accounts>,
    persons: Arc<K1Persons>,
    registrations: Mutex<()>,
}

impl K1Users {
    pub fn new(accounts: Arc<K1Accounts>, persons: Arc<K1Persons>) -> Self {
        Self {
            accounts,
            persons,
            registrations: Mutex::new(()),
        }
    }

    pub fn register(&self, code: &InviteCode, user: NewUser) -> Result<User, String> {
        let _registration = lock_unpoison(&self.registrations);
        if let Some(account) = self.accounts.find_by_username(user.username())? {
            if !account_matches(&account, &user) {
                return Err("username already registered".to_owned());
            }
            return self.resolve_user(account);
        }
        let NewUser { account, full_name } = user;
        let person_id = self.persons.create(full_name)?;
        let account = NewAccount::new(
            account.username(),
            person_id,
            account.public_key(),
            account.tos_revision(),
            account.tos_digest(),
            account.acceptance_signature(),
        )?;
        let account = self.accounts.register(code, account)?;
        self.resolve_user(account)
    }

    pub fn get(&self, user_id: UserId) -> Result<Option<User>, String> {
        self.accounts
            .get(user_id)?
            .map(|account| self.resolve_user(account))
            .transpose()
    }

    pub fn find_by_username(&self, username: &str) -> Result<Option<User>, String> {
        self.accounts
            .find_by_username(username)?
            .map(|account| self.resolve_user(account))
            .transpose()
    }

    fn resolve_user(&self, account: Account) -> Result<User, String> {
        let view = self
            .persons
            .read(account.person_id())?
            .ok_or_else(|| "account person anchor is missing".to_owned())?;
        Ok(User {
            account,
            person_id: view.person_id,
            full_name: view.name,
        })
    }
}

fn account_matches(account: &Account, user: &NewUser) -> bool {
    account.username() == user.username()
        && account.public_key() == user.public_key()
        && account.tos_revision() == user.tos_revision()
        && account.tos_digest() == user.tos_digest()
        && account.acceptance_signature() == user.acceptance_signature()
}

fn validate_full_name(full_name: &str) -> Result<(), String> {
    if !(1..=128).contains(&full_name.len()) {
        return Err("person name must be 1 through 128 UTF-8 bytes".to_owned());
    }
    if full_name.chars().any(char::is_control) {
        return Err("person name must not contain control characters".to_owned());
    }
    if full_name.chars().all(char::is_whitespace) {
        return Err("person name must contain a non-whitespace character".to_owned());
    }
    Ok(())
}

fn copy_string(value: &str) -> Result<String, String> {
    let mut copy = String::new();
    copy.try_reserve_exact(value.len())
        .map_err(|_| "memory allocation failed".to_owned())?;
    copy.push_str(value);
    Ok(copy)
}

fn zero_person_id() -> PersonId {
    PersonId::from_tx_id(TxId::from_bytes([0; 12]))
}

fn lock_unpoison<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
    match mutex.lock() {
        Ok(guard) => guard,
        Err(poisoned) => poisoned.into_inner(),
    }
}