use chrono::{DateTime, Utc};
use lunar_lib::{
database::{
CompareAndSwapTransaction, Createable, CustomTransactionError, DatabaseEntry, Db, DbIdExt,
Deleteable, Entry, TransactionError,
},
id::Id,
};
use selene_common::account::Permissions;
use serde::{Deserialize, Serialize};
use crate::{
accounts::{AccountCreationError, NAME_ID_MAP, password::Password},
database::{Searchable, Selene},
library::image_art::ImageArt,
};
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct Account {
pub id: Id<Self>,
pub name: String,
pub password: Password,
pub created: DateTime<Utc>,
pub modified: DateTime<Utc>,
pub last_login: DateTime<Utc>,
pub permissions: Permissions,
pub profile_picture: Option<Id<ImageArt>>,
}
impl DatabaseEntry for Account {
type DbInner = Selene;
const VERSION_NUMBER: u32 = 1;
const TREE_NAME: &str = "accounts";
}
impl Createable for Account {
type CreateArgs = (String, Password);
type Err = AccountCreationError;
fn create(
(name, password): Self::CreateArgs,
cas_tx: &mut CompareAndSwapTransaction<Self::DbInner>,
) -> Result<Entry<Self>, CustomTransactionError<Self::Err>> {
let len = name.chars().take(33).count();
if !(3..=32).contains(&len) {
return Err(CustomTransactionError::Closure(
AccountCreationError::InvalidLength(len as u32),
));
}
let id = loop {
let id = Id::generate();
if !id.tx_check(cas_tx)? {
break id;
}
};
let now = Utc::now();
let account = Self {
id,
name: name.clone(),
password,
created: now,
modified: now,
last_login: now,
permissions: Permissions::default(),
profile_picture: None,
};
let index = cas_tx.get_or_new_index(NAME_ID_MAP);
let index_key = name.as_bytes();
if index.check(index_key)? {
return Err(CustomTransactionError::Closure(
AccountCreationError::DuplicateName(name),
));
}
index.upsert(index_key, &*id)?;
Ok(account.to_entry(id))
}
}
impl Deleteable for Account {
fn unlink_references(
_old: Entry<Self>,
_cas_tx: &mut CompareAndSwapTransaction<Self::DbInner>,
) -> Result<(), TransactionError> {
Ok(())
}
}
impl Searchable for Account {
const SEARCH_INDEX: &'static str = "account_search";
fn search_name(&self) -> Option<&str> {
Some(&*self.name)
}
}
impl Account {
pub fn get_by_name(
username: impl AsRef<str>,
db: &Db<Selene>,
) -> Result<Option<Self>, TransactionError> {
let name = username.as_ref();
let Some(account) = Self::search(db, name, 1, 0)?.into_iter().next() else {
return Ok(None);
};
if account.name.to_ascii_lowercase() == name {
Ok(Some(account.into_item()))
} else {
Ok(None)
}
}
}