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(),
}
}