tetratto-core 18.0.1

The core behind Tetratto
Documentation
use std::fmt::Display;

use serde::{Deserialize, Serialize};
use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp};
use crate::{
    database::app_data::{FREE_DATA_LIMIT, PASS_DATA_LIMIT},
    model::{auth::User, oauth::AppScope, permissions::SecondaryPermission},
};

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[derive(Default)]
pub enum AppQuota {
    /// The app is limited to 5 grants.
    #[default]
    Limited,
    /// The app is allowed to maintain an unlimited number of grants.
    Unlimited,
}


/// The storage limit for apps where the owner has a developer pass.
///
/// Free users are always limited to 500 KB.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[derive(Default)]
pub enum DeveloperPassStorageQuota {
    /// The app is limited to 25 MB.
    #[default]
    Tier1,
    /// The app is limited to 50 MB.
    Tier2,
    /// The app is limited to 100 MB.
    Tier3,
    /// The app is not limited.
    Unlimited,
}


impl DeveloperPassStorageQuota {
    pub fn limit(&self) -> usize {
        match self {
            DeveloperPassStorageQuota::Tier1 => 26214400,
            DeveloperPassStorageQuota::Tier2 => 52428800,
            DeveloperPassStorageQuota::Tier3 => 104857600,
            DeveloperPassStorageQuota::Unlimited => usize::MAX,
        }
    }
}

/// An app is required to request grants on user accounts.
///
/// Users must approve grants through a web portal.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ThirdPartyApp {
    pub id: usize,
    pub created: usize,
    /// The ID of the owner of the app.
    pub owner: usize,
    /// The name of the app.
    pub title: String,
    /// The URL of the app's homepage.
    pub homepage: String,
    /// The redirect URL for the app.
    ///
    /// Upon accepting a grant request, the user will be redirected to this URL
    /// with a query parameter named `token`, which should be saved by the app
    /// for future authentication.
    ///
    /// The developer dashboard lists the URL you should send users to in order to
    /// create a grant on their account in the information section under the label
    /// "Grant URL".
    ///
    /// Any search parameters sent with your grant URL (such as an internal user ID)
    /// will also be sent back when the user is redirected to your redirect URL.
    ///
    /// You can use this behaviour to keep track of what user you should save the grant
    /// token under.
    ///
    /// 1. Redirect user to grant URL with their ID: `{grant_url}?my_app_user_id={id}`
    /// 2. In your redirect endpoint, read that ID and the added `token` parameter to
    /// store the `token` under the given `my_app_user_id`
    ///
    /// The redirect URL will also have a `verifier` search parameter appended.
    /// This verifier is required to refresh the grant's token (which is what is
    /// used in the `Atto-Grant` cookie).
    ///
    /// Tokens only last a week after they were generated (with the verifier),
    /// but you can refresh them by sending a request to:
    /// `{tetratto}/api/v1/auth/user/{user_id}/grants/{app_id}/refresh`.
    ///
    /// Tetratto will generate the verifier and challenge for you. The challenge
    /// is an SHA-256 hashed + base64 url encoded version of the verifier. This means
    /// if the verifier doesn't match, it won't pass the challenge.
    ///
    /// Requests to API endpoints using your grant token should be sent with a
    /// cookie (in the `Cookie` or `X-Cookie` header) named `Atto-Grant`. This cookie should
    /// contain the token you received from either the initial connection,
    /// or a token refresh.
    pub redirect: String,
    /// The app's quota status, which determines how many grants the app is allowed to maintain.
    pub quota_status: AppQuota,
    /// If the app is banned. A banned app cannot use any of its grants.
    pub banned: bool,
    /// The number of accepted grants the app maintains.
    pub grants: usize,
    /// The scopes used for every grant the app maintains.
    ///
    /// These scopes are only cloned into **new** grants created for the app.
    /// An app *cannot* change scopes and have them affect users who already have the
    /// app connected. Users must delete the app's grant and authenticate it again
    /// to update their scopes.
    ///
    /// Your app should handle informing users when scopes change.
    pub scopes: Vec<AppScope>,
    /// The app's secret API key (for app_data access).
    pub api_key: String,
    /// The number of bytes the app's app_data rows are using.
    pub data_used: usize,
    /// The app's storage capacity.
    pub storage_capacity: DeveloperPassStorageQuota,
}

impl ThirdPartyApp {
    /// Create a new [`ThirdPartyApp`].
    pub fn new(title: String, owner: usize, homepage: String, redirect: String) -> Self {
        Self {
            id: Snowflake::new().to_string().parse::<usize>().unwrap(),
            created: unix_epoch_timestamp(),
            owner,
            title,
            homepage,
            redirect,
            quota_status: AppQuota::default(),
            banned: false,
            grants: 0,
            scopes: Vec::new(),
            api_key: String::new(),
            data_used: 0,
            storage_capacity: DeveloperPassStorageQuota::default(),
        }
    }
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AppData {
    pub id: usize,
    pub app: usize,
    pub key: String,
    pub value: String,
}

impl AppData {
    /// Create a new [`AppData`].
    pub fn new(app: usize, key: String, value: String) -> Self {
        Self {
            id: Snowflake::new().to_string().parse::<usize>().unwrap(),
            app,
            key,
            value,
        }
    }

    /// Get the data limit of a given user.
    pub fn user_limit(user: &User, app: &ThirdPartyApp) -> usize {
        if user
            .secondary_permissions
            .check(SecondaryPermission::DEVELOPER_PASS)
        {
            if app.storage_capacity != DeveloperPassStorageQuota::Tier1 {
                app.storage_capacity.limit()
            } else {
                PASS_DATA_LIMIT
            }
        } else {
            FREE_DATA_LIMIT
        }
    }
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum AppDataSelectQuery {
    KeyIs(String),
    KeyLike(String),
    ValueLike(String),
    LikeJson(String, String),
}

impl Display for AppDataSelectQuery {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&match self {
            Self::KeyIs(k) => k.to_owned(),
            Self::KeyLike(k) => k.to_owned(),
            Self::ValueLike(v) => v.to_owned(),
            Self::LikeJson(k, v) => format!("%\"{k}\":\"{v}\"%"),
        })
    }
}

impl AppDataSelectQuery {
    pub fn selector(&self) -> String {
        match self {
            AppDataSelectQuery::KeyIs(_) => "k = $1".to_string(),
            AppDataSelectQuery::KeyLike(_) => "k LIKE $1".to_string(),
            AppDataSelectQuery::ValueLike(_) => "v LIKE $1".to_string(),
            AppDataSelectQuery::LikeJson(_, _) => "v LIKE $1".to_string(),
        }
    }
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum AppDataSelectMode {
    /// Select a single row (with offset).
    One(usize),
    /// Select multiple rows at once.
    ///
    /// `(limit, offset)`
    Many(usize, usize),
    /// Select multiple rows at once.
    ///
    /// `(order by top level key, limit, offset)`
    ManyJson(String, usize, usize),
}

impl Display for AppDataSelectMode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&match self {
            Self::One(offset) => format!("LIMIT 1 OFFSET {offset}"),
            Self::Many(limit, offset) => {
                format!(
                    "ORDER BY k DESC LIMIT {} OFFSET {offset}",
                    if *limit > 24 { 24 } else { *limit }
                )
            }
            Self::ManyJson(order_by_top_level_key, limit, offset) => {
                format!(
                    "ORDER BY v::jsonb->>'{order_by_top_level_key}' DESC LIMIT {} OFFSET {offset}",
                    if *limit > 24 { 24 } else { *limit }
                )
            }
        })
    }
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AppDataQuery {
    pub app: usize,
    pub query: AppDataSelectQuery,
    pub mode: AppDataSelectMode,
}

impl Display for AppDataQuery {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&format!(
            "SELECT * FROM app_data WHERE app = {} AND %q% {}",
            self.app, self.mode
        ))
    }
}

#[derive(Serialize, Deserialize)]
pub enum AppDataQueryResult {
    One(AppData),
    Many(Vec<AppData>),
}