From 9aed5de097e404a96c9bee28abe0e55a29f3a918 Mon Sep 17 00:00:00 2001 From: trisua Date: Sun, 20 Jul 2025 20:19:33 -0400 Subject: [PATCH] add: extended app storage limits --- crates/app/src/langs/en-US.toml | 1 + crates/app/src/public/html/developer/app.lisp | 42 +++++++++++++++++++ crates/app/src/routes/api/v1/app_data.rs | 4 +- crates/app/src/routes/api/v1/apps.rs | 31 +++++++++++++- crates/app/src/routes/api/v1/mod.rs | 13 +++++- crates/app/src/routes/pages/developer.rs | 2 +- crates/core/src/database/apps.rs | 9 ++-- .../src/database/drivers/sql/create_apps.sql | 3 +- .../drivers/sql/version_migrations.sql | 4 ++ crates/core/src/model/apps.rs | 42 +++++++++++++++++-- crates/core/src/model/auth.rs | 1 - crates/shared/src/markdown.rs | 11 +++-- 12 files changed, 143 insertions(+), 20 deletions(-) diff --git a/crates/app/src/langs/en-US.toml b/crates/app/src/langs/en-US.toml index 3246755..bd692f3 100644 --- a/crates/app/src/langs/en-US.toml +++ b/crates/app/src/langs/en-US.toml @@ -258,6 +258,7 @@ version = "1.0.0" "developer:label.change_homepage" = "Change homepage" "developer:label.change_redirect" = "Change redirect URL" "developer:label.change_quota_status" = "Change quota status" +"developer:label.change_storage_capacity" = "Change storage capacity" "developer:label.manage_scopes" = "Manage scopes" "developer:label.scopes" = "Scopes" "developer:label.guides_and_help" = "Guides & help" diff --git a/crates/app/src/public/html/developer/app.lisp b/crates/app/src/public/html/developer/app.lisp index b1661e8..d19fb10 100644 --- a/crates/app/src/public/html/developer/app.lisp +++ b/crates/app/src/public/html/developer/app.lisp @@ -44,6 +44,28 @@ ("value" "Unlimited") ("selected" "{% if app.quota_status == 'Unlimited' -%}true{% else %}false{%- endif %}") (text "Unlimited"))))) + (div + ("class" "card-nest") + (div + ("class" "card small flex items-center gap-2") + (icon (text "database-zap")) + (b (str (text "developer:label.change_storage_capacity")))) + (div + ("class" "card") + (select + ("onchange" "save_storage_capacity(event)") + (option + ("value" "Tier1") + ("selected" "{% if app.storage_capacity == 'Tier1' -%}true{% else %}false{%- endif %}") + (text "Tier 1 (25 MB)")) + (option + ("value" "Tier2") + ("selected" "{% if app.storage_capacity == 'Tier2' -%}true{% else %}false{%- endif %}") + (text "Tier 2 (50 MB)")) + (option + ("value" "Tier3") + ("selected" "{% if app.storage_capacity == 'Tier3' -%}true{% else %}false{%- endif %}") + (text "Tier 3 (100 MB)"))))) (text "{%- endif %}") (div ("class" "card-nest") @@ -232,6 +254,26 @@ }); }; + globalThis.save_storage_capacity = (event) => { + const selected = event.target.selectedOptions[0]; + fetch(\"/api/v1/apps/{{ app.id }}/storage_capacity\", { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + storage_capacity: selected.value, + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + }; + globalThis.change_title = async (e) => { e.preventDefault(); diff --git a/crates/app/src/routes/api/v1/app_data.rs b/crates/app/src/routes/api/v1/app_data.rs index 5e0182e..b5fa212 100644 --- a/crates/app/src/routes/api/v1/app_data.rs +++ b/crates/app/src/routes/api/v1/app_data.rs @@ -72,7 +72,7 @@ pub async fn create_request( // check size let new_size = app.data_used + req.value.len(); - if new_size > AppData::user_limit(&owner) { + if new_size > AppData::user_limit(&owner, &app) { return Json(Error::AppHitStorageLimit.into()); } @@ -155,7 +155,7 @@ pub async fn update_value_request( let size_without = app.data_used - app_data.value.len(); let new_size = size_without + req.value.len(); - if new_size > AppData::user_limit(&owner) { + if new_size > AppData::user_limit(&owner, &app) { return Json(Error::AppHitStorageLimit.into()); } diff --git a/crates/app/src/routes/api/v1/apps.rs b/crates/app/src/routes/api/v1/apps.rs index 3b5cd60..eac16ba 100644 --- a/crates/app/src/routes/api/v1/apps.rs +++ b/crates/app/src/routes/api/v1/apps.rs @@ -15,7 +15,7 @@ use tetratto_core::model::{ ApiReturn, Error, }; use tetratto_shared::{hash::random_id, unix_epoch_timestamp}; -use super::CreateApp; +use super::{CreateApp, UpdateAppStorageCapacity}; pub async fn create_request( jar: CookieJar, @@ -138,6 +138,35 @@ pub async fn update_quota_status_request( } } +pub async fn update_storage_capacity_request( + jar: CookieJar, + Extension(data): Extension, + Path(id): Path, + Json(req): Json, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + if !user.permissions.check(FinePermission::MANAGE_APPS) { + return Json(Error::NotAllowed.into()); + } + + match data + .update_app_storage_capacity(id, req.storage_capacity) + .await + { + Ok(_) => Json(ApiReturn { + ok: true, + message: "App updated".to_string(), + payload: (), + }), + Err(e) => Json(e.into()), + } +} + pub async fn update_scopes_request( jar: CookieJar, Extension(data): Extension, diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs index 6b56e9b..91f4bfa 100644 --- a/crates/app/src/routes/api/v1/mod.rs +++ b/crates/app/src/routes/api/v1/mod.rs @@ -20,9 +20,9 @@ use axum::{ routing::{any, delete, get, post, put}, Router, }; -use serde::{Deserialize}; +use serde::Deserialize; use tetratto_core::model::{ - apps::{AppDataSelectMode, AppDataSelectQuery, AppQuota}, + apps::{AppDataSelectMode, AppDataSelectQuery, AppQuota, DeveloperPassStorageQuota}, auth::AchievementName, communities::{ CommunityContext, CommunityJoinAccess, CommunityReadAccess, CommunityWriteAccess, @@ -432,6 +432,10 @@ pub fn routes() -> Router { "/apps/{id}/quota_status", post(apps::update_quota_status_request), ) + .route( + "/apps/{id}/storage_capacity", + post(apps::update_storage_capacity_request), + ) .route("/apps/{id}/scopes", post(apps::update_scopes_request)) .route("/apps/{id}/grant", post(apps::grant_request)) .route("/apps/{id}/roll", post(apps::roll_api_key_request)) @@ -1031,6 +1035,11 @@ pub struct UpdateAppQuotaStatus { pub quota_status: AppQuota, } +#[derive(Deserialize)] +pub struct UpdateAppStorageCapacity { + pub storage_capacity: DeveloperPassStorageQuota, +} + #[derive(Deserialize)] pub struct UpdateAppScopes { pub scopes: Vec, diff --git a/crates/app/src/routes/pages/developer.rs b/crates/app/src/routes/pages/developer.rs index 0d421f7..a9e5f92 100644 --- a/crates/app/src/routes/pages/developer.rs +++ b/crates/app/src/routes/pages/developer.rs @@ -62,7 +62,7 @@ pub async fn app_request( )); } - let data_limit = AppData::user_limit(&user); + let data_limit = AppData::user_limit(&user, &app); let lang = get_lang!(jar, data.0); let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await; diff --git a/crates/core/src/database/apps.rs b/crates/core/src/database/apps.rs index b605cb6..72334a8 100644 --- a/crates/core/src/database/apps.rs +++ b/crates/core/src/database/apps.rs @@ -1,6 +1,6 @@ use oiseau::cache::Cache; use crate::model::{ - apps::{AppQuota, ThirdPartyApp}, + apps::{AppQuota, ThirdPartyApp, DeveloperPassStorageQuota}, auth::User, oauth::AppScope, permissions::{FinePermission, SecondaryPermission}, @@ -25,6 +25,7 @@ impl DataManager { scopes: serde_json::from_str(&get!(x->9(String))).unwrap(), api_key: get!(x->10(String)), data_used: get!(x->11(i32)) as usize, + storage_capacity: serde_json::from_str(&get!(x->12(String))).unwrap(), } } @@ -95,7 +96,7 @@ impl DataManager { let res = execute!( &conn, - "INSERT INTO apps VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)", + "INSERT INTO apps VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)", params![ &(data.id as i64), &(data.created as i64), @@ -108,7 +109,8 @@ impl DataManager { &(data.grants as i32), &serde_json::to_string(&data.scopes).unwrap(), &data.api_key, - &(data.data_used as i32) + &(data.data_used as i32), + &serde_json::to_string(&data.storage_capacity).unwrap(), ] ); @@ -167,6 +169,7 @@ impl DataManager { auto_method!(update_app_quota_status(AppQuota)@get_app_by_id -> "UPDATE apps SET quota_status = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_app); auto_method!(update_app_scopes(Vec)@get_app_by_id:FinePermission::MANAGE_APPS; -> "UPDATE apps SET scopes = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_app); auto_method!(update_app_api_key(&str)@get_app_by_id -> "UPDATE apps SET api_key = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_app); + auto_method!(update_app_storage_capacity(DeveloperPassStorageQuota)@get_app_by_id -> "UPDATE apps SET storage_capacity = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_app); auto_method!(update_app_data_used(i32)@get_app_by_id -> "UPDATE apps SET data_used = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_app); auto_method!(add_app_data_used(i32)@get_app_by_id -> "UPDATE apps SET data_used = data_used + $1 WHERE id = $2" --cache-key-tmpl=cache_clear_app); diff --git a/crates/core/src/database/drivers/sql/create_apps.sql b/crates/core/src/database/drivers/sql/create_apps.sql index d01ed41..70f2b8d 100644 --- a/crates/core/src/database/drivers/sql/create_apps.sql +++ b/crates/core/src/database/drivers/sql/create_apps.sql @@ -9,5 +9,6 @@ CREATE TABLE IF NOT EXISTS apps ( banned INT NOT NULL, grants INT NOT NULL, scopes TEXT NOT NULL, - data_used INT NOT NULL CHECK (data_used >= 0) + data_used INT NOT NULL CHECK (data_used >= 0), + storage_capacity TEXT NOT NULL ) diff --git a/crates/core/src/database/drivers/sql/version_migrations.sql b/crates/core/src/database/drivers/sql/version_migrations.sql index c0c863a..1988e63 100644 --- a/crates/core/src/database/drivers/sql/version_migrations.sql +++ b/crates/core/src/database/drivers/sql/version_migrations.sql @@ -5,3 +5,7 @@ ADD COLUMN IF NOT EXISTS channel_mutes TEXT DEFAULT '[]'; -- users is_deactivated ALTER TABLE users ADD COLUMN IF NOT EXISTS is_deactivated INT DEFAULT 0; + +-- apps storage_capacity +ALTER TABLE apps +ADD COLUMN IF NOT EXISTS storage_capacity TEXT DEFAULT '"Tier1"'; diff --git a/crates/core/src/model/apps.rs b/crates/core/src/model/apps.rs index 309d850..470bc8b 100644 --- a/crates/core/src/model/apps.rs +++ b/crates/core/src/model/apps.rs @@ -21,6 +21,35 @@ impl Default for AppQuota { } } +/// 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)] +pub enum DeveloperPassStorageQuota { + /// The app is limited to 25 MB. + Tier1, + /// The app is limited to 50 MB. + Tier2, + /// The app is limited to 100 MB. + Tier3, +} + +impl Default for DeveloperPassStorageQuota { + fn default() -> Self { + Self::Tier1 + } +} + +impl DeveloperPassStorageQuota { + pub fn limit(&self) -> usize { + match self { + DeveloperPassStorageQuota::Tier1 => 26214400, + DeveloperPassStorageQuota::Tier2 => 52428800, + DeveloperPassStorageQuota::Tier3 => 104857600, + } + } +} + /// An app is required to request grants on user accounts. /// /// Users must approve grants through a web portal. @@ -90,6 +119,8 @@ pub struct ThirdPartyApp { 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 { @@ -102,12 +133,13 @@ impl ThirdPartyApp { title, homepage, redirect, - quota_status: AppQuota::Limited, + quota_status: AppQuota::default(), banned: false, grants: 0, scopes: Vec::new(), api_key: String::new(), data_used: 0, + storage_capacity: DeveloperPassStorageQuota::default(), } } } @@ -132,12 +164,16 @@ impl AppData { } /// Get the data limit of a given user. - pub fn user_limit(user: &User) -> usize { + pub fn user_limit(user: &User, app: &ThirdPartyApp) -> usize { if user .secondary_permissions .check(SecondaryPermission::DEVELOPER_PASS) { - PASS_DATA_LIMIT + if app.storage_capacity != DeveloperPassStorageQuota::Tier1 { + app.storage_capacity.limit() + } else { + PASS_DATA_LIMIT + } } else { FREE_DATA_LIMIT } diff --git a/crates/core/src/model/auth.rs b/crates/core/src/model/auth.rs index 37d2bf9..c7d7338 100644 --- a/crates/core/src/model/auth.rs +++ b/crates/core/src/model/auth.rs @@ -1,5 +1,4 @@ use std::collections::HashMap; - use super::{ oauth::AuthGrant, permissions::{FinePermission, SecondaryPermission}, diff --git a/crates/shared/src/markdown.rs b/crates/shared/src/markdown.rs index d0a9c56..1d1626a 100644 --- a/crates/shared/src/markdown.rs +++ b/crates/shared/src/markdown.rs @@ -159,12 +159,11 @@ pub fn parse_alignment(input: &str) -> String { let mut buffer = String::new(); for line in lines { - match line { - "```" => { - is_in_pre = !is_in_pre; - output.push_str(&format!("{line}\n")); - } - _ => parse_alignment_line(line, &mut output, &mut buffer, is_in_pre), + if line.starts_with("```") { + is_in_pre = !is_in_pre; + output.push_str(&format!("{line}\n")); + } else { + parse_alignment_line(line, &mut output, &mut buffer, is_in_pre) } }