tetratto-core 18.0.1

The core behind Tetratto
Documentation
use oiseau::cache::Cache;
use crate::model::{
    apps::{AppQuota, ThirdPartyApp, DeveloperPassStorageQuota},
    auth::User,
    oauth::AppScope,
    permissions::{FinePermission, SecondaryPermission},
    Error, Result,
};
use crate::{auto_method, DataManager};
use oiseau::{PostgresRow, execute, get, query_row, query_rows, params};

impl DataManager {
    /// Get a [`ThirdPartyApp`] from an SQL row.
    pub(crate) fn get_app_from_row(x: &PostgresRow) -> ThirdPartyApp {
        ThirdPartyApp {
            id: get!(x->0(i64)) as usize,
            created: get!(x->1(i64)) as usize,
            owner: get!(x->2(i64)) as usize,
            title: get!(x->3(String)),
            homepage: get!(x->4(String)),
            redirect: get!(x->5(String)),
            quota_status: serde_json::from_str(&get!(x->6(String))).unwrap(),
            banned: get!(x->7(i32)) as i8 == 1,
            grants: get!(x->8(i32)) as usize,
            scopes: serde_json::from_str(&get!(x->9(String))).unwrap(),
            api_key: get!(x->10(String)),
            data_used: get!(x->11(i32)) as usize,
            storage_capacity: serde_json::from_str(&get!(x->12(String))).unwrap(),
        }
    }

    auto_method!(get_app_by_id(usize as i64)@get_app_from_row -> "SELECT * FROM apps WHERE id = $1" --name="app" --returns=ThirdPartyApp --cache-key-tmpl="atto.app:{}");
    auto_method!(get_app_by_api_key(&str)@get_app_from_row -> "SELECT * FROM apps WHERE api_key = $1" --name="app" --returns=ThirdPartyApp --cache-key-tmpl="atto.app_k:{}");

    /// Get all apps by user.
    ///
    /// # Arguments
    /// * `id` - the ID of the user to fetch apps for
    pub async fn get_apps_by_owner(&self, id: usize) -> Result<Vec<ThirdPartyApp>> {
        let conn = match self.0.connect().await {
            Ok(c) => c,
            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
        };

        let res = query_rows!(
            &conn,
            "SELECT * FROM apps WHERE owner = $1 ORDER BY created DESC",
            &[&(id as i64)],
            |x| { Self::get_app_from_row(x) }
        );

        if res.is_err() {
            return Err(Error::GeneralNotFound("app".to_string()));
        }

        Ok(res.unwrap())
    }

    const MAXIMUM_FREE_APPS: usize = 1;

    /// Create a new app in the database.
    ///
    /// # Arguments
    /// * `data` - a mock [`ThirdPartyApp`] object to insert
    pub async fn create_app(&self, data: ThirdPartyApp) -> Result<ThirdPartyApp> {
        // check values
        if data.title.trim().len() < 2 {
            return Err(Error::DataTooShort("title".to_string()));
        } else if data.title.len() > 32 {
            return Err(Error::DataTooLong("title".to_string()));
        }

        // check number of apps
        let owner = self.get_user_by_id(data.owner).await?;

        if !owner
            .secondary_permissions
            .check(SecondaryPermission::DEVELOPER_PASS)
        {
            let apps = self
                .get_table_row_count_where("apps", &format!("owner = {}", owner.id))
                .await? as usize;

            if apps >= Self::MAXIMUM_FREE_APPS {
                return Err(Error::MiscError(
                    "You already have the maximum number of apps you can have".to_string(),
                ));
            }
        }

        // ...
        let conn = match self.0.connect().await {
            Ok(c) => c,
            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
        };

        let res = execute!(
            &conn,
            "INSERT INTO apps VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)",
            params![
                &(data.id as i64),
                &(data.created as i64),
                &(data.owner as i64),
                &data.title,
                &data.homepage,
                &data.redirect,
                &serde_json::to_string(&data.quota_status).unwrap(),
                &{ if data.banned { 1 } else { 0 } },
                &(data.grants as i32),
                &serde_json::to_string(&data.scopes).unwrap(),
                &data.api_key,
                &(data.data_used as i32),
                &serde_json::to_string(&data.storage_capacity).unwrap(),
            ]
        );

        if let Err(e) = res {
            return Err(Error::DatabaseError(e.to_string()));
        }

        Ok(data)
    }

    pub async fn delete_app(&self, id: usize, user: &User) -> Result<()> {
        let app = self.get_app_by_id(id).await?;

        // check user permission
        if user.id != app.owner && !user.permissions.check(FinePermission::MANAGE_APPS) {
            return Err(Error::NotAllowed);
        }

        // ...
        let conn = match self.0.connect().await {
            Ok(c) => c,
            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
        };

        let res = execute!(&conn, "DELETE FROM apps WHERE id = $1", &[&(id as i64)]);

        if let Err(e) = res {
            return Err(Error::DatabaseError(e.to_string()));
        }

        self.cache_clear_app(&app).await;

        // remove data
        let res = execute!(
            &conn,
            "DELETE FROM app_data WHERE app = $1",
            &[&(id as i64)]
        );

        if let Err(e) = res {
            return Err(Error::DatabaseError(e.to_string()));
        }

        // ...
        Ok(())
    }

    pub async fn cache_clear_app(&self, app: &ThirdPartyApp) {
        self.0.1.remove(format!("atto.app:{}", app.id)).await;
        self.0.1.remove(format!("atto.app_k:{}", app.api_key)).await;
    }

    auto_method!(update_app_title(&str)@get_app_by_id:FinePermission::MANAGE_APPS; -> "UPDATE apps SET title = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_app);
    auto_method!(update_app_homepage(&str)@get_app_by_id:FinePermission::MANAGE_APPS; -> "UPDATE apps SET homepage = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_app);
    auto_method!(update_app_redirect(&str)@get_app_by_id:FinePermission::MANAGE_APPS; -> "UPDATE apps SET redirect = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_app);
    auto_method!(update_app_quota_status(AppQuota)@get_app_by_id -> "UPDATE apps SET quota_status = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_app);
    auto_method!(update_app_scopes(Vec<AppScope>)@get_app_by_id:FinePermission::MANAGE_APPS; -> "UPDATE apps SET scopes = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_app);
    auto_method!(update_app_api_key(&str)@get_app_by_id -> "UPDATE apps SET api_key = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_app);
    auto_method!(update_app_storage_capacity(DeveloperPassStorageQuota)@get_app_by_id -> "UPDATE apps SET storage_capacity = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_app);

    auto_method!(update_app_data_used(i32)@get_app_by_id -> "UPDATE apps SET data_used = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_app);
    auto_method!(add_app_data_used(i32)@get_app_by_id -> "UPDATE apps SET data_used = data_used + $1 WHERE id = $2" --cache-key-tmpl=cache_clear_app);

    auto_method!(incr_app_grants()@get_app_by_id -> "UPDATE apps SET grants = grants + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_app --incr);
    auto_method!(decr_app_grants()@get_app_by_id -> "UPDATE apps SET grants = grants - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_app --decr=grants);
}