tetratto_core/model/
apps.rs

1use std::fmt::Display;
2
3use serde::{Deserialize, Serialize};
4use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp};
5use crate::{
6    database::app_data::{FREE_DATA_LIMIT, PASS_DATA_LIMIT},
7    model::{auth::User, oauth::AppScope, permissions::SecondaryPermission},
8};
9
10#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
11pub enum AppQuota {
12    /// The app is limited to 5 grants.
13    Limited,
14    /// The app is allowed to maintain an unlimited number of grants.
15    Unlimited,
16}
17
18impl Default for AppQuota {
19    fn default() -> Self {
20        Self::Limited
21    }
22}
23
24/// The storage limit for apps where the owner has a developer pass.
25///
26/// Free users are always limited to 500 KB.
27#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
28pub enum DeveloperPassStorageQuota {
29    /// The app is limited to 25 MB.
30    Tier1,
31    /// The app is limited to 50 MB.
32    Tier2,
33    /// The app is limited to 100 MB.
34    Tier3,
35    /// The app is not limited.
36    Unlimited,
37}
38
39impl Default for DeveloperPassStorageQuota {
40    fn default() -> Self {
41        Self::Tier1
42    }
43}
44
45impl DeveloperPassStorageQuota {
46    pub fn limit(&self) -> usize {
47        match self {
48            DeveloperPassStorageQuota::Tier1 => 26214400,
49            DeveloperPassStorageQuota::Tier2 => 52428800,
50            DeveloperPassStorageQuota::Tier3 => 104857600,
51            DeveloperPassStorageQuota::Unlimited => usize::MAX,
52        }
53    }
54}
55
56/// An app is required to request grants on user accounts.
57///
58/// Users must approve grants through a web portal.
59#[derive(Serialize, Deserialize, Debug, Clone)]
60pub struct ThirdPartyApp {
61    pub id: usize,
62    pub created: usize,
63    /// The ID of the owner of the app.
64    pub owner: usize,
65    /// The name of the app.
66    pub title: String,
67    /// The URL of the app's homepage.
68    pub homepage: String,
69    /// The redirect URL for the app.
70    ///
71    /// Upon accepting a grant request, the user will be redirected to this URL
72    /// with a query parameter named `token`, which should be saved by the app
73    /// for future authentication.
74    ///
75    /// The developer dashboard lists the URL you should send users to in order to
76    /// create a grant on their account in the information section under the label
77    /// "Grant URL".
78    ///
79    /// Any search parameters sent with your grant URL (such as an internal user ID)
80    /// will also be sent back when the user is redirected to your redirect URL.
81    ///
82    /// You can use this behaviour to keep track of what user you should save the grant
83    /// token under.
84    ///
85    /// 1. Redirect user to grant URL with their ID: `{grant_url}?my_app_user_id={id}`
86    /// 2. In your redirect endpoint, read that ID and the added `token` parameter to
87    /// store the `token` under the given `my_app_user_id`
88    ///
89    /// The redirect URL will also have a `verifier` search parameter appended.
90    /// This verifier is required to refresh the grant's token (which is what is
91    /// used in the `Atto-Grant` cookie).
92    ///
93    /// Tokens only last a week after they were generated (with the verifier),
94    /// but you can refresh them by sending a request to:
95    /// `{tetratto}/api/v1/auth/user/{user_id}/grants/{app_id}/refresh`.
96    ///
97    /// Tetratto will generate the verifier and challenge for you. The challenge
98    /// is an SHA-256 hashed + base64 url encoded version of the verifier. This means
99    /// if the verifier doesn't match, it won't pass the challenge.
100    ///
101    /// Requests to API endpoints using your grant token should be sent with a
102    /// cookie (in the `Cookie` or `X-Cookie` header) named `Atto-Grant`. This cookie should
103    /// contain the token you received from either the initial connection,
104    /// or a token refresh.
105    pub redirect: String,
106    /// The app's quota status, which determines how many grants the app is allowed to maintain.
107    pub quota_status: AppQuota,
108    /// If the app is banned. A banned app cannot use any of its grants.
109    pub banned: bool,
110    /// The number of accepted grants the app maintains.
111    pub grants: usize,
112    /// The scopes used for every grant the app maintains.
113    ///
114    /// These scopes are only cloned into **new** grants created for the app.
115    /// An app *cannot* change scopes and have them affect users who already have the
116    /// app connected. Users must delete the app's grant and authenticate it again
117    /// to update their scopes.
118    ///
119    /// Your app should handle informing users when scopes change.
120    pub scopes: Vec<AppScope>,
121    /// The app's secret API key (for app_data access).
122    pub api_key: String,
123    /// The number of bytes the app's app_data rows are using.
124    pub data_used: usize,
125    /// The app's storage capacity.
126    pub storage_capacity: DeveloperPassStorageQuota,
127}
128
129impl ThirdPartyApp {
130    /// Create a new [`ThirdPartyApp`].
131    pub fn new(title: String, owner: usize, homepage: String, redirect: String) -> Self {
132        Self {
133            id: Snowflake::new().to_string().parse::<usize>().unwrap(),
134            created: unix_epoch_timestamp(),
135            owner,
136            title,
137            homepage,
138            redirect,
139            quota_status: AppQuota::default(),
140            banned: false,
141            grants: 0,
142            scopes: Vec::new(),
143            api_key: String::new(),
144            data_used: 0,
145            storage_capacity: DeveloperPassStorageQuota::default(),
146        }
147    }
148}
149
150#[derive(Serialize, Deserialize, Debug, Clone)]
151pub struct AppData {
152    pub id: usize,
153    pub app: usize,
154    pub key: String,
155    pub value: String,
156}
157
158impl AppData {
159    /// Create a new [`AppData`].
160    pub fn new(app: usize, key: String, value: String) -> Self {
161        Self {
162            id: Snowflake::new().to_string().parse::<usize>().unwrap(),
163            app,
164            key,
165            value,
166        }
167    }
168
169    /// Get the data limit of a given user.
170    pub fn user_limit(user: &User, app: &ThirdPartyApp) -> usize {
171        if user
172            .secondary_permissions
173            .check(SecondaryPermission::DEVELOPER_PASS)
174        {
175            if app.storage_capacity != DeveloperPassStorageQuota::Tier1 {
176                app.storage_capacity.limit()
177            } else {
178                PASS_DATA_LIMIT
179            }
180        } else {
181            FREE_DATA_LIMIT
182        }
183    }
184}
185
186#[derive(Serialize, Deserialize, Debug, Clone)]
187pub enum AppDataSelectQuery {
188    KeyIs(String),
189    KeyLike(String),
190    ValueLike(String),
191    LikeJson(String, String),
192}
193
194impl Display for AppDataSelectQuery {
195    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196        f.write_str(&match self {
197            Self::KeyIs(k) => k.to_owned(),
198            Self::KeyLike(k) => k.to_owned(),
199            Self::ValueLike(v) => v.to_owned(),
200            Self::LikeJson(k, v) => format!("%\"{k}\":\"{v}\"%"),
201        })
202    }
203}
204
205impl AppDataSelectQuery {
206    pub fn selector(&self) -> String {
207        match self {
208            AppDataSelectQuery::KeyIs(_) => format!("k = $1"),
209            AppDataSelectQuery::KeyLike(_) => format!("k LIKE $1"),
210            AppDataSelectQuery::ValueLike(_) => format!("v LIKE $1"),
211            AppDataSelectQuery::LikeJson(_, _) => format!("v LIKE $1"),
212        }
213    }
214}
215
216#[derive(Serialize, Deserialize, Debug, Clone)]
217pub enum AppDataSelectMode {
218    /// Select a single row (with offset).
219    One(usize),
220    /// Select multiple rows at once.
221    ///
222    /// `(limit, offset)`
223    Many(usize, usize),
224    /// Select multiple rows at once.
225    ///
226    /// `(order by top level key, limit, offset)`
227    ManyJson(String, usize, usize),
228}
229
230impl Display for AppDataSelectMode {
231    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
232        f.write_str(&match self {
233            Self::One(offset) => format!("LIMIT 1 OFFSET {offset}"),
234            Self::Many(limit, offset) => {
235                format!(
236                    "ORDER BY k DESC LIMIT {} OFFSET {offset}",
237                    if *limit > 24 { 24 } else { *limit }
238                )
239            }
240            Self::ManyJson(order_by_top_level_key, limit, offset) => {
241                format!(
242                    "ORDER BY v::jsonb->>'{order_by_top_level_key}' DESC LIMIT {} OFFSET {offset}",
243                    if *limit > 24 { 24 } else { *limit }
244                )
245            }
246        })
247    }
248}
249
250#[derive(Serialize, Deserialize, Debug, Clone)]
251pub struct AppDataQuery {
252    pub app: usize,
253    pub query: AppDataSelectQuery,
254    pub mode: AppDataSelectMode,
255}
256
257impl Display for AppDataQuery {
258    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
259        f.write_str(&format!(
260            "SELECT * FROM app_data WHERE app = {} AND %q% {}",
261            self.app, self.mode
262        ))
263    }
264}
265
266#[derive(Serialize, Deserialize)]
267pub enum AppDataQueryResult {
268    One(AppData),
269    Many(Vec<AppData>),
270}