tetratto_core/database/
apps.rs

1use oiseau::cache::Cache;
2use crate::model::{
3    apps::{AppQuota, ThirdPartyApp, DeveloperPassStorageQuota},
4    auth::User,
5    oauth::AppScope,
6    permissions::{FinePermission, SecondaryPermission},
7    Error, Result,
8};
9use crate::{auto_method, DataManager};
10use oiseau::{PostgresRow, execute, get, query_row, query_rows, params};
11
12impl DataManager {
13    /// Get a [`ThirdPartyApp`] from an SQL row.
14    pub(crate) fn get_app_from_row(x: &PostgresRow) -> ThirdPartyApp {
15        ThirdPartyApp {
16            id: get!(x->0(i64)) as usize,
17            created: get!(x->1(i64)) as usize,
18            owner: get!(x->2(i64)) as usize,
19            title: get!(x->3(String)),
20            homepage: get!(x->4(String)),
21            redirect: get!(x->5(String)),
22            quota_status: serde_json::from_str(&get!(x->6(String))).unwrap(),
23            banned: get!(x->7(i32)) as i8 == 1,
24            grants: get!(x->8(i32)) as usize,
25            scopes: serde_json::from_str(&get!(x->9(String))).unwrap(),
26            api_key: get!(x->10(String)),
27            data_used: get!(x->11(i32)) as usize,
28            storage_capacity: serde_json::from_str(&get!(x->12(String))).unwrap(),
29        }
30    }
31
32    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:{}");
33    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:{}");
34
35    /// Get all apps by user.
36    ///
37    /// # Arguments
38    /// * `id` - the ID of the user to fetch apps for
39    pub async fn get_apps_by_owner(&self, id: usize) -> Result<Vec<ThirdPartyApp>> {
40        let conn = match self.0.connect().await {
41            Ok(c) => c,
42            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
43        };
44
45        let res = query_rows!(
46            &conn,
47            "SELECT * FROM apps WHERE owner = $1 ORDER BY created DESC",
48            &[&(id as i64)],
49            |x| { Self::get_app_from_row(x) }
50        );
51
52        if res.is_err() {
53            return Err(Error::GeneralNotFound("app".to_string()));
54        }
55
56        Ok(res.unwrap())
57    }
58
59    const MAXIMUM_FREE_APPS: usize = 1;
60
61    /// Create a new app in the database.
62    ///
63    /// # Arguments
64    /// * `data` - a mock [`ThirdPartyApp`] object to insert
65    pub async fn create_app(&self, data: ThirdPartyApp) -> Result<ThirdPartyApp> {
66        // check values
67        if data.title.trim().len() < 2 {
68            return Err(Error::DataTooShort("title".to_string()));
69        } else if data.title.len() > 32 {
70            return Err(Error::DataTooLong("title".to_string()));
71        }
72
73        // check number of apps
74        let owner = self.get_user_by_id(data.owner).await?;
75
76        if !owner
77            .secondary_permissions
78            .check(SecondaryPermission::DEVELOPER_PASS)
79        {
80            let apps = self
81                .get_table_row_count_where("apps", &format!("owner = {}", owner.id))
82                .await? as usize;
83
84            if apps >= Self::MAXIMUM_FREE_APPS {
85                return Err(Error::MiscError(
86                    "You already have the maximum number of apps you can have".to_string(),
87                ));
88            }
89        }
90
91        // ...
92        let conn = match self.0.connect().await {
93            Ok(c) => c,
94            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
95        };
96
97        let res = execute!(
98            &conn,
99            "INSERT INTO apps VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)",
100            params![
101                &(data.id as i64),
102                &(data.created as i64),
103                &(data.owner as i64),
104                &data.title,
105                &data.homepage,
106                &data.redirect,
107                &serde_json::to_string(&data.quota_status).unwrap(),
108                &{ if data.banned { 1 } else { 0 } },
109                &(data.grants as i32),
110                &serde_json::to_string(&data.scopes).unwrap(),
111                &data.api_key,
112                &(data.data_used as i32),
113                &serde_json::to_string(&data.storage_capacity).unwrap(),
114            ]
115        );
116
117        if let Err(e) = res {
118            return Err(Error::DatabaseError(e.to_string()));
119        }
120
121        Ok(data)
122    }
123
124    pub async fn delete_app(&self, id: usize, user: &User) -> Result<()> {
125        let app = self.get_app_by_id(id).await?;
126
127        // check user permission
128        if user.id != app.owner && !user.permissions.check(FinePermission::MANAGE_APPS) {
129            return Err(Error::NotAllowed);
130        }
131
132        // ...
133        let conn = match self.0.connect().await {
134            Ok(c) => c,
135            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
136        };
137
138        let res = execute!(&conn, "DELETE FROM apps WHERE id = $1", &[&(id as i64)]);
139
140        if let Err(e) = res {
141            return Err(Error::DatabaseError(e.to_string()));
142        }
143
144        self.cache_clear_app(&app).await;
145
146        // remove data
147        let res = execute!(
148            &conn,
149            "DELETE FROM app_data WHERE app = $1",
150            &[&(id as i64)]
151        );
152
153        if let Err(e) = res {
154            return Err(Error::DatabaseError(e.to_string()));
155        }
156
157        // ...
158        Ok(())
159    }
160
161    pub async fn cache_clear_app(&self, app: &ThirdPartyApp) {
162        self.0.1.remove(format!("atto.app:{}", app.id)).await;
163        self.0.1.remove(format!("atto.app_k:{}", app.api_key)).await;
164    }
165
166    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);
167    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);
168    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);
169    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);
170    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);
171    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);
172    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);
173
174    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);
175    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);
176
177    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);
178    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);
179}