2025-07-17 01:30:27 -04:00
|
|
|
use std::fmt::Display;
|
|
|
|
|
2025-06-14 14:45:52 -04:00
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp};
|
2025-06-14 20:26:54 -04:00
|
|
|
use crate::model::oauth::AppScope;
|
2025-06-14 14:45:52 -04:00
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
|
|
|
pub enum AppQuota {
|
|
|
|
/// The app is limited to 5 grants.
|
|
|
|
Limited,
|
|
|
|
/// The app is allowed to maintain an unlimited number of grants.
|
|
|
|
Unlimited,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for AppQuota {
|
|
|
|
fn default() -> Self {
|
|
|
|
Self::Limited
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 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.
|
2025-06-14 20:26:54 -04:00
|
|
|
///
|
|
|
|
/// 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` header) named `Atto-Grant`. This cookie should
|
|
|
|
/// contain the token you received from either the initial connection,
|
|
|
|
/// or a token refresh.
|
2025-06-14 14:45:52 -04:00
|
|
|
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,
|
2025-06-14 20:26:54 -04:00
|
|
|
/// 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>,
|
2025-06-14 14:45:52 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
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(),
|
2025-06-14 20:26:54 -04:00
|
|
|
created: unix_epoch_timestamp(),
|
2025-06-14 14:45:52 -04:00
|
|
|
owner,
|
|
|
|
title,
|
|
|
|
homepage,
|
|
|
|
redirect,
|
|
|
|
quota_status: AppQuota::Limited,
|
|
|
|
banned: false,
|
|
|
|
grants: 0,
|
2025-06-14 20:26:54 -04:00
|
|
|
scopes: Vec::new(),
|
2025-06-14 14:45:52 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2025-07-17 01:30:27 -04:00
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
|
|
pub struct AppData {
|
|
|
|
pub id: usize,
|
|
|
|
pub owner: usize,
|
|
|
|
pub app: usize,
|
|
|
|
pub key: String,
|
|
|
|
pub value: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl AppData {
|
|
|
|
/// Create a new [`AppData`].
|
|
|
|
pub fn new(owner: usize, app: usize, key: String, value: String) -> Self {
|
|
|
|
Self {
|
|
|
|
id: Snowflake::new().to_string().parse::<usize>().unwrap(),
|
|
|
|
owner,
|
|
|
|
app,
|
|
|
|
key,
|
|
|
|
value,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
|
|
pub enum AppDataSelectQuery {
|
|
|
|
Like(String, String),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Display for AppDataSelectQuery {
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
f.write_str(&match self {
|
|
|
|
Self::Like(k, v) => format!("%\"{k}\":\"{v}\"%"),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
|
|
pub enum AppDataSelectMode {
|
|
|
|
/// Select a single row.
|
|
|
|
One,
|
|
|
|
/// Select multiple rows at once.
|
|
|
|
///
|
|
|
|
/// `(order by top level key, limit, offset)`
|
|
|
|
Many(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 => "LIMIT 1".to_string(),
|
|
|
|
Self::Many(order_by_top_level_key, limit, offset) => {
|
|
|
|
format!(
|
|
|
|
"ORDER BY v::jsonb->>'{order_by_top_level_key}' LIMIT {limit} OFFSET {offset}"
|
|
|
|
)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[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 v LIKE $1 {}",
|
|
|
|
self.app, self.mode
|
|
|
|
))
|
|
|
|
}
|
|
|
|
}
|