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}