selene-core 0.9.0-rc.1

Backend for selene-server
Documentation
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)
        }
    }
}