Skip to main content

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