add: app_data api
This commit is contained in:
parent
5c520f4308
commit
f423daf2fc
38 changed files with 410 additions and 91 deletions
|
@ -253,6 +253,9 @@ version = "1.0.0"
|
|||
"developer:label.manage_scopes" = "Manage scopes"
|
||||
"developer:label.scopes" = "Scopes"
|
||||
"developer:label.guides_and_help" = "Guides & help"
|
||||
"developer:label.secret_key" = "Secret key"
|
||||
"developer:label.roll_key" = "Roll key"
|
||||
"developer:label.data_usage" = "Data usage"
|
||||
"developer:action.delete" = "Delete app"
|
||||
"developer:action.authorize" = "Authorize"
|
||||
|
||||
|
|
|
@ -419,3 +419,20 @@ macro_rules! ignore_users_gen {
|
|||
.concat()
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! get_app_from_key {
|
||||
($db:ident, $jar:ident) => {
|
||||
if let Some(token) = $jar.get("Atto-Secret-Key") {
|
||||
match $db
|
||||
.get_app_by_api_key(&token.to_string().replace("Atto-Secret-Key=", ""))
|
||||
.await
|
||||
{
|
||||
Ok(x) => Some(x),
|
||||
Err(_) => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -404,7 +404,7 @@ select:focus {
|
|||
.poll_bar {
|
||||
background-color: var(--color-primary);
|
||||
border-radius: var(--radius);
|
||||
height: 25px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.poll_option {
|
||||
|
@ -413,6 +413,22 @@ select:focus {
|
|||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.progress_bar {
|
||||
background: var(--color-super-lowered);
|
||||
border-radius: var(--circle);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.progress_bar .poll_bar {
|
||||
border-radius: var(--circle);
|
||||
height: 14px;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
--color: #c9b1bc;
|
||||
appearance: none;
|
||||
|
|
|
@ -159,7 +159,6 @@
|
|||
(text "{{ icon \"notepad-text-dashed\" }}"))
|
||||
(text "{%- endif %} {%- endif %}")
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ text \"communities:action.create\" }}"))))))
|
||||
(text "{% if not quoting -%}")
|
||||
(script
|
||||
|
|
|
@ -29,7 +29,6 @@
|
|||
("minlength" "2")
|
||||
("maxlength" "32")))
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ text \"communities:action.create\" }}"))))
|
||||
(text "{% if list|length >= 4 -%} {{ components::supporter_ad(body=\"Become a supporter to create up to 10 communities!\") }} {%- endif %} {%- endif %}")
|
||||
(div
|
||||
|
|
|
@ -39,7 +39,6 @@
|
|||
("class" "flex gap-2")
|
||||
(text "{{ components::emoji_picker(element_id=\"content\", render_dialog=true) }} {% if is_supporter -%} {{ components::file_picker(files_list_id=\"files_list\") }} {% endif %}")
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ text \"requests:label.answer\" }}")))))
|
||||
(text "{%- endif %}")
|
||||
(div
|
||||
|
|
|
@ -28,7 +28,6 @@
|
|||
("maxlength" "32")
|
||||
("value" "{{ text }}")))
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ text \"dialog:action.continue\" }}"))))
|
||||
(div
|
||||
("class" "card-nest")
|
||||
|
|
|
@ -135,7 +135,6 @@
|
|||
("required" "")
|
||||
("minlength" "2")))
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ icon \"check\" }}")
|
||||
(span
|
||||
(text "{{ text \"general:action.save\" }}"))))))
|
||||
|
@ -190,7 +189,6 @@
|
|||
("accept" "image/png,image/jpeg,image/avif,image/webp,image/gif")
|
||||
("class" "w-content"))
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ icon \"check\" }}"))))
|
||||
(div
|
||||
("class" "card-nest")
|
||||
|
@ -213,7 +211,6 @@
|
|||
("accept" "image/png,image/jpeg,image/avif,image/webp")
|
||||
("class" "w-content"))
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ icon \"check\" }}")))
|
||||
(span
|
||||
("class" "fade")
|
||||
|
@ -245,7 +242,6 @@
|
|||
("required" "")
|
||||
("minlength" "18")))
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ text \"communities:action.select\" }}")))))
|
||||
(div
|
||||
("class" "card flex flex-col gap-2 w-full")
|
||||
|
@ -296,7 +292,6 @@
|
|||
("minlength" "2")
|
||||
("maxlength" "32")))
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ text \"communities:action.create\" }}"))))
|
||||
(text "{% for channel in channels %}")
|
||||
(div
|
||||
|
|
|
@ -779,7 +779,6 @@
|
|||
(div
|
||||
("class" "flex gap-2")
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ text \"communities:action.create\" }}"))
|
||||
|
||||
(text "{% if drawing_enabled -%}")
|
||||
|
@ -1879,7 +1878,6 @@
|
|||
("id" "join_or_leave")
|
||||
(text "{% if not is_owner -%} {% if not is_joined -%} {% if not is_pending %}")
|
||||
(button
|
||||
("class" "primary")
|
||||
("onclick" "join_community()")
|
||||
(text "{{ icon \"circle-plus\" }}")
|
||||
(span
|
||||
|
|
|
@ -10,11 +10,27 @@
|
|||
(div
|
||||
("id" "manage_fields")
|
||||
("class" "card lowered flex flex-col gap-2")
|
||||
(div
|
||||
("class" "card-nest")
|
||||
(div
|
||||
("class" "card small flex items-center gap-2")
|
||||
(icon (text "database"))
|
||||
(b (str (text "developer:label.data_usage"))))
|
||||
(div
|
||||
("class" "card flex flex-col gap-2")
|
||||
(p ("class" "fade") (text "App data keys are not included in this metric, only stored values count towards your limit."))
|
||||
(text "{% set percentage = (data_limit / app.data_used) * 100 %}")
|
||||
(div ("class" "progress_bar") (div ("class" "poll_bar") ("style" "width: {{ percentage }}%")))
|
||||
(div
|
||||
("class" "w-full flex justify-between items-center")
|
||||
(span (text "{{ app.data_used|filesizeformat }}"))
|
||||
(span (text "{{ data_limit|filesizeformat }}")))))
|
||||
(text "{% if is_helper -%}")
|
||||
(div
|
||||
("class" "card-nest")
|
||||
(div
|
||||
("class" "card small")
|
||||
("class" "card small flex items-center gap-2")
|
||||
(icon (text "infinity"))
|
||||
(b (str (text "developer:label.change_quota_status"))))
|
||||
(div
|
||||
("class" "card")
|
||||
|
@ -32,7 +48,8 @@
|
|||
(div
|
||||
("class" "card-nest")
|
||||
(div
|
||||
("class" "card small")
|
||||
("class" "card small flex items-center gap-2")
|
||||
(icon (text "pencil"))
|
||||
(b (str (text "developer:label.change_title"))))
|
||||
(form
|
||||
("class" "card flex flex-col gap-2")
|
||||
|
@ -50,14 +67,14 @@
|
|||
("required" "")
|
||||
("minlength" "2")))
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ icon \"check\" }}")
|
||||
(span
|
||||
(text "{{ text \"general:action.save\" }}")))))
|
||||
(div
|
||||
("class" "card-nest")
|
||||
(div
|
||||
("class" "card small")
|
||||
("class" "card small flex items-center gap-2")
|
||||
(icon (text "house"))
|
||||
(b (str (text "developer:label.change_homepage"))))
|
||||
(form
|
||||
("class" "card flex flex-col gap-2")
|
||||
|
@ -75,14 +92,14 @@
|
|||
("required" "")
|
||||
("minlength" "2")))
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ icon \"check\" }}")
|
||||
(span
|
||||
(text "{{ text \"general:action.save\" }}")))))
|
||||
(div
|
||||
("class" "card-nest")
|
||||
(div
|
||||
("class" "card small")
|
||||
("class" "card small flex items-center gap-2")
|
||||
(icon (text "goal"))
|
||||
(b (str (text "developer:label.change_redirect"))))
|
||||
(form
|
||||
("class" "card flex flex-col gap-2")
|
||||
|
@ -100,14 +117,14 @@
|
|||
("required" "")
|
||||
("minlength" "2")))
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ icon \"check\" }}")
|
||||
(span
|
||||
(text "{{ text \"general:action.save\" }}")))))
|
||||
(div
|
||||
("class" "card-nest")
|
||||
(div
|
||||
("class" "card small")
|
||||
("class" "card small flex items-center gap-2")
|
||||
(icon (text "telescope"))
|
||||
(b (str (text "developer:label.manage_scopes"))))
|
||||
(form
|
||||
("class" "card flex flex-col gap-2")
|
||||
|
@ -140,10 +157,22 @@
|
|||
(icon (text "external-link")) (text "Docs"))))
|
||||
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ icon \"check\" }}")
|
||||
(span
|
||||
(text "{{ text \"general:action.save\" }}"))))))
|
||||
(text "{{ text \"general:action.save\" }}")))))
|
||||
(div
|
||||
("class" "card-nest")
|
||||
(div
|
||||
("class" "card small flex items-center gap-2")
|
||||
(icon (text "rotate-ccw-key"))
|
||||
(b (str (text "developer:label.secret_key"))))
|
||||
(div
|
||||
("class" "card flex flex-col gap-2")
|
||||
(p ("class" "fade") (text "Your app's API key can only be seen once, so don't lose it. Rolling the key will invalidate the old one."))
|
||||
(pre (code ("id" "new_key")))
|
||||
(button
|
||||
("onclick" "roll_key()")
|
||||
(str (text "developer:label.roll_key"))))))
|
||||
(div
|
||||
("class" "card flex flex-col gap-2")
|
||||
(ul
|
||||
|
@ -323,6 +352,31 @@
|
|||
});
|
||||
};
|
||||
|
||||
globalThis.roll_key = async () => {
|
||||
if (
|
||||
!(await trigger(\"atto::confirm\", [
|
||||
\"Are you sure you would like to do this?\",
|
||||
]))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(\"/api/v1/apps/{{ app.id }}/roll\", {
|
||||
method: \"POST\",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
trigger(\"atto::toast\", [
|
||||
res.ok ? \"success\" : \"error\",
|
||||
res.message,
|
||||
]);
|
||||
|
||||
if (res.ok) {
|
||||
document.getElementById(\"new_key\").innerText = res.payload;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
globalThis.delete_app = async () => {
|
||||
if (
|
||||
!(await trigger(\"atto::confirm\", [
|
||||
|
|
|
@ -57,7 +57,6 @@
|
|||
("minlength" "2")
|
||||
("maxlength" "32")))
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ text \"communities:action.create\" }}"))))
|
||||
|
||||
; app listing
|
||||
|
|
|
@ -30,7 +30,6 @@
|
|||
("minlength" "2")
|
||||
("maxlength" "32")))
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ text \"communities:action.create\" }}"))))
|
||||
(text "{% else %}")
|
||||
(text "{{ components::supporter_ad(body=\"Become a supporter to create forges!\") }}")
|
||||
|
|
|
@ -253,7 +253,6 @@
|
|||
("required" "")
|
||||
("minlength" "2")))
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ icon \"check\" }}")
|
||||
(span
|
||||
(text "{{ text \"general:action.save\" }}")))))))
|
||||
|
|
|
@ -59,7 +59,6 @@
|
|||
(option ("value" "{{ tld }}") (text ".{{ tld|lower }}"))
|
||||
(text "{%- endfor %}")))
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ text \"communities:action.create\" }}"))
|
||||
|
||||
(details
|
||||
|
|
|
@ -45,7 +45,6 @@
|
|||
("minlength" "2")
|
||||
("maxlength" "32")))
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ text \"communities:action.create\" }}"))))
|
||||
(text "{%- endif %}")
|
||||
(div
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
(p (text "You'll find out what each achievement is when you get it, so look around!"))
|
||||
(hr)
|
||||
(span (b (text "Your progress: ")) (text "{{ percentage|round(method=\"floor\", precision=2) }}%"))
|
||||
(div ("class" "poll_bar") ("style" "width: {{ percentage }}%"))))
|
||||
(div ("class" "progress_bar") (div ("class" "poll_bar") ("style" "width: {{ percentage }}%")))))
|
||||
|
||||
(div
|
||||
("class" "card-nest")
|
||||
|
|
|
@ -132,7 +132,6 @@
|
|||
(text "{{ text \"auth:action.ip_block\" }}")))
|
||||
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ text \"requests:label.answer\" }}")))))
|
||||
(text "{% endfor %}")))
|
||||
|
||||
|
|
|
@ -28,7 +28,6 @@
|
|||
("required" "")
|
||||
("minlength" "16")))
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ text \"communities:action.create\" }}")))))
|
||||
|
||||
(script
|
||||
|
|
|
@ -298,7 +298,6 @@
|
|||
("minlength" "2")
|
||||
(text "{{ profile.ban_reason|remove_script_tags|safe }}")))
|
||||
(button
|
||||
("class" "primary")
|
||||
(str (text "general:action.save")))))
|
||||
(div
|
||||
("class" "card-nest w-full")
|
||||
|
@ -396,6 +395,7 @@
|
|||
MANAGE_DOMAINS: 1 << 2,
|
||||
MANAGE_SERVICES: 1 << 3,
|
||||
MANAGE_PRODUCTS: 1 << 4,
|
||||
DEVELOPER_PASS: 1 << 5,
|
||||
},
|
||||
\"secondary_role\",
|
||||
\"add_permission_to_secondary_role\",
|
||||
|
|
|
@ -37,7 +37,6 @@
|
|||
("minlength" "2")
|
||||
("maxlength" "4096")))
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ text \"communities:action.create\" }}"))))
|
||||
(div
|
||||
("class" "card-nest")
|
||||
|
|
|
@ -80,7 +80,6 @@
|
|||
("class" "flex gap-2")
|
||||
(text "{{ components::emoji_picker(element_id=\"content\", render_dialog=true) }} {% if is_supporter -%} {{ components::file_picker(files_list_id=\"files_list\") }} {% endif %}")
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ text \"communities:action.create\" }}")))))
|
||||
(text "{%- endif %}")
|
||||
(div
|
||||
|
@ -279,7 +278,6 @@
|
|||
("class" "flex gap-2")
|
||||
(text "{{ components::emoji_picker(element_id=\"new_content\", render_dialog=false) }}")
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ text \"general:action.save\" }}")))))
|
||||
(script
|
||||
(text "async function edit_post_from_form(e) {
|
||||
|
|
|
@ -276,7 +276,6 @@
|
|||
("required" "")
|
||||
("minlength" "2")))
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ icon \"check\" }}")
|
||||
(span
|
||||
(text "{{ text \"general:action.save\" }}"))))))
|
||||
|
@ -305,7 +304,6 @@
|
|||
("minlength" "6")
|
||||
("autocomplete" "off")))
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ icon \"trash\" }}")
|
||||
(span
|
||||
(text "{{ text \"general:action.delete\" }}")))))
|
||||
|
@ -419,7 +417,6 @@
|
|||
("minlength" "6")
|
||||
("autocomplete" "off")))
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ icon \"check\" }}")
|
||||
(span
|
||||
(text "{{ text \"general:action.save\" }}")))))))))
|
||||
|
@ -908,7 +905,6 @@
|
|||
("accept" "image/png,image/jpeg,image/avif,image/webp,image/gif")
|
||||
("class" "w-content"))
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ icon \"check\" }}")))
|
||||
(span
|
||||
("class" "fade")
|
||||
|
@ -936,7 +932,6 @@
|
|||
("accept" "image/png,image/jpeg,image/avif,image/webp,image/gif")
|
||||
("class" "w-content"))
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ icon \"check\" }}")))
|
||||
(span
|
||||
("class" "fade")
|
||||
|
@ -1054,7 +1049,6 @@
|
|||
("class" "card w-full flex flex-wrap gap-2")
|
||||
("ui_ident" "import_export")
|
||||
(button
|
||||
("class" "primary")
|
||||
("onclick" "import_theme_settings()")
|
||||
(text "{{ icon \"upload\" }}")
|
||||
(span
|
||||
|
|
|
@ -29,7 +29,6 @@
|
|||
("minlength" "2")
|
||||
("maxlength" "32")))
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ text \"communities:action.create\" }}"))))
|
||||
(text "{%- endif %}")
|
||||
(div
|
||||
|
|
|
@ -114,7 +114,6 @@
|
|||
("required" "")
|
||||
("minlength" "2")))
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ icon \"check\" }}")
|
||||
(span
|
||||
(text "{{ text \"general:action.save\" }}"))))))
|
||||
|
|
136
crates/app/src/routes/api/v1/app_data.rs
Normal file
136
crates/app/src/routes/api/v1/app_data.rs
Normal file
|
@ -0,0 +1,136 @@
|
|||
use crate::{
|
||||
get_app_from_key,
|
||||
routes::api::v1::{InsertAppData, QueryAppData, UpdateAppDataValue},
|
||||
State,
|
||||
};
|
||||
use axum::{Extension, Json, extract::Path, response::IntoResponse};
|
||||
use axum_extra::extract::CookieJar;
|
||||
use tetratto_core::model::{
|
||||
apps::{AppData, AppDataQuery},
|
||||
ApiReturn, Error,
|
||||
};
|
||||
|
||||
pub async fn query_request(
|
||||
jar: CookieJar,
|
||||
Extension(data): Extension<State>,
|
||||
Json(req): Json<QueryAppData>,
|
||||
) -> impl IntoResponse {
|
||||
let data = &(data.read().await).0;
|
||||
let app = match get_app_from_key!(data, jar) {
|
||||
Some(x) => x,
|
||||
None => return Json(Error::NotAllowed.into()),
|
||||
};
|
||||
|
||||
match data
|
||||
.query_app_data(AppDataQuery {
|
||||
app: app.id,
|
||||
query: req.query,
|
||||
mode: req.mode,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(x) => Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "Success".to_string(),
|
||||
payload: Some(x),
|
||||
}),
|
||||
Err(e) => Json(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_request(
|
||||
jar: CookieJar,
|
||||
Extension(data): Extension<State>,
|
||||
Json(req): Json<InsertAppData>,
|
||||
) -> impl IntoResponse {
|
||||
let data = &(data.read().await).0;
|
||||
let app = match get_app_from_key!(data, jar) {
|
||||
Some(x) => x,
|
||||
None => return Json(Error::NotAllowed.into()),
|
||||
};
|
||||
|
||||
let owner = match data.get_user_by_id(app.owner).await {
|
||||
Ok(x) => x,
|
||||
Err(e) => return Json(e.into()),
|
||||
};
|
||||
|
||||
// check size
|
||||
let new_size = app.data_used + req.value.len();
|
||||
if new_size > AppData::user_limit(&owner) {
|
||||
return Json(Error::AppHitStorageLimit.into());
|
||||
}
|
||||
|
||||
// ...
|
||||
match data
|
||||
.create_app_data(AppData::new(app.id, req.key, req.value))
|
||||
.await
|
||||
{
|
||||
Ok(s) => Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "App created".to_string(),
|
||||
payload: s.id.to_string(),
|
||||
}),
|
||||
Err(e) => Json(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_value_request(
|
||||
jar: CookieJar,
|
||||
Extension(data): Extension<State>,
|
||||
Path(id): Path<usize>,
|
||||
Json(req): Json<UpdateAppDataValue>,
|
||||
) -> impl IntoResponse {
|
||||
let data = &(data.read().await).0;
|
||||
let app = match get_app_from_key!(data, jar) {
|
||||
Some(x) => x,
|
||||
None => return Json(Error::NotAllowed.into()),
|
||||
};
|
||||
|
||||
let owner = match data.get_user_by_id(app.owner).await {
|
||||
Ok(x) => x,
|
||||
Err(e) => return Json(e.into()),
|
||||
};
|
||||
|
||||
let app_data = match data.get_app_data_by_id(id).await {
|
||||
Ok(x) => x,
|
||||
Err(e) => return Json(e.into()),
|
||||
};
|
||||
|
||||
// check size
|
||||
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) {
|
||||
return Json(Error::AppHitStorageLimit.into());
|
||||
}
|
||||
|
||||
// ...
|
||||
match data.update_app_data_value(id, &req.value).await {
|
||||
Ok(_) => Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "Data updated".to_string(),
|
||||
payload: (),
|
||||
}),
|
||||
Err(e) => Json(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_request(
|
||||
jar: CookieJar,
|
||||
Extension(data): Extension<State>,
|
||||
Path(id): Path<usize>,
|
||||
) -> impl IntoResponse {
|
||||
let data = &(data.read().await).0;
|
||||
if get_app_from_key!(data, jar).is_none() {
|
||||
return Json(Error::NotAllowed.into());
|
||||
}
|
||||
|
||||
match data.delete_app_data(id).await {
|
||||
Ok(_) => Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "Data deleted".to_string(),
|
||||
payload: (),
|
||||
}),
|
||||
Err(e) => Json(e.into()),
|
||||
}
|
||||
}
|
|
@ -239,3 +239,34 @@ pub async fn grant_request(
|
|||
Err(e) => Json(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn roll_api_key_request(
|
||||
jar: CookieJar,
|
||||
Extension(data): Extension<State>,
|
||||
Path(id): Path<usize>,
|
||||
) -> 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()),
|
||||
};
|
||||
|
||||
let app = match data.get_app_by_id(id).await {
|
||||
Ok(x) => x,
|
||||
Err(e) => return Json(e.into()),
|
||||
};
|
||||
|
||||
if user.id != app.owner {
|
||||
return Json(Error::NotAllowed.into());
|
||||
}
|
||||
|
||||
let new_key = tetratto_shared::hash::random_id_salted_len(32);
|
||||
match data.update_app_api_key(id, &new_key).await {
|
||||
Ok(_) => Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "App updated".to_string(),
|
||||
payload: Some(new_key),
|
||||
}),
|
||||
Err(e) => Json(e.into()),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
pub mod app_data;
|
||||
pub mod apps;
|
||||
pub mod auth;
|
||||
pub mod channels;
|
||||
|
@ -19,9 +20,9 @@ use axum::{
|
|||
routing::{any, delete, get, post, put},
|
||||
Router,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize};
|
||||
use tetratto_core::model::{
|
||||
apps::AppQuota,
|
||||
apps::{AppDataSelectMode, AppDataSelectQuery, AppQuota},
|
||||
auth::AchievementName,
|
||||
communities::{
|
||||
CommunityContext, CommunityJoinAccess, CommunityReadAccess, CommunityWriteAccess,
|
||||
|
@ -32,7 +33,7 @@ use tetratto_core::model::{
|
|||
littleweb::{DomainData, DomainTld, ServiceFsEntry},
|
||||
oauth::AppScope,
|
||||
permissions::{FinePermission, SecondaryPermission},
|
||||
products::{ProductType, ProductPrice},
|
||||
products::{ProductPrice, ProductType},
|
||||
reactions::AssetType,
|
||||
stacks::{StackMode, StackPrivacy, StackSort},
|
||||
};
|
||||
|
@ -419,6 +420,7 @@ pub fn routes() -> Router {
|
|||
)
|
||||
// apps
|
||||
.route("/apps", post(apps::create_request))
|
||||
.route("/apps/{id}", delete(apps::delete_request))
|
||||
.route("/apps/{id}/title", post(apps::update_title_request))
|
||||
.route("/apps/{id}/homepage", post(apps::update_homepage_request))
|
||||
.route("/apps/{id}/redirect", post(apps::update_redirect_request))
|
||||
|
@ -427,8 +429,13 @@ pub fn routes() -> Router {
|
|||
post(apps::update_quota_status_request),
|
||||
)
|
||||
.route("/apps/{id}/scopes", post(apps::update_scopes_request))
|
||||
.route("/apps/{id}", delete(apps::delete_request))
|
||||
.route("/apps/{id}/grant", post(apps::grant_request))
|
||||
.route("/apps/{id}/roll", post(apps::roll_api_key_request))
|
||||
// app data
|
||||
.route("/app_data", post(app_data::create_request))
|
||||
.route("/app_data/query", post(app_data::query_request))
|
||||
.route("/app_data/{id}", delete(app_data::delete_request))
|
||||
.route("/app_data/{id}/value", post(app_data::update_value_request))
|
||||
// warnings
|
||||
.route("/warnings/{id}", get(auth::user_warnings::get_request))
|
||||
.route("/warnings/{id}", post(auth::user_warnings::create_request))
|
||||
|
@ -1148,3 +1155,20 @@ pub struct UpdateProductPrice {
|
|||
pub struct UpdateUploadAlt {
|
||||
pub alt: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateAppDataValue {
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct InsertAppData {
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct QueryAppData {
|
||||
pub query: AppDataSelectQuery,
|
||||
pub mode: AppDataSelectMode,
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ use axum::{
|
|||
Extension,
|
||||
};
|
||||
use axum_extra::extract::CookieJar;
|
||||
use tetratto_core::model::{permissions::FinePermission, Error};
|
||||
use tetratto_core::model::{apps::AppData, permissions::FinePermission, Error};
|
||||
|
||||
/// `/developer`
|
||||
pub async fn home_request(jar: CookieJar, Extension(data): Extension<State>) -> impl IntoResponse {
|
||||
|
@ -62,9 +62,13 @@ pub async fn app_request(
|
|||
));
|
||||
}
|
||||
|
||||
let data_limit = AppData::user_limit(&user);
|
||||
|
||||
let lang = get_lang!(jar, data.0);
|
||||
let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await;
|
||||
|
||||
context.insert("app", &app);
|
||||
context.insert("data_limit", &data_limit);
|
||||
|
||||
// return
|
||||
Ok(Html(data.1.render("developer/app.html", &context).unwrap()))
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
use oiseau::cache::Cache;
|
||||
use crate::model::{apps::AppData, auth::User, permissions::FinePermission, Error, Result};
|
||||
use crate::model::apps::{AppDataQuery, AppDataQueryResult, AppDataSelectMode, AppDataSelectQuery};
|
||||
use crate::model::{apps::AppData, permissions::FinePermission, Error, Result};
|
||||
use crate::{auto_method, DataManager};
|
||||
use oiseau::{PostgresRow, execute, get, query_row, query_rows, params};
|
||||
|
||||
use oiseau::PostgresRow;
|
||||
|
||||
use oiseau::{execute, get, query_rows, params};
|
||||
pub const FREE_DATA_LIMIT: usize = 512_000;
|
||||
pub const PASS_DATA_LIMIT: usize = 5_242_880;
|
||||
|
||||
impl DataManager {
|
||||
/// Get a [`AppData`] from an SQL row.
|
||||
pub(crate) fn get_app_data_from_row(x: &PostgresRow) -> AppData {
|
||||
AppData {
|
||||
id: get!(x->0(i64)) as usize,
|
||||
owner: get!(x->1(i64)) as usize,
|
||||
app: get!(x->2(i64)) as usize,
|
||||
key: get!(x->3(String)),
|
||||
value: get!(x->4(String)),
|
||||
|
@ -48,24 +48,39 @@ impl DataManager {
|
|||
///
|
||||
/// # Arguments
|
||||
/// * `id` - the ID of the user to fetch app_data for
|
||||
pub async fn get_app_data_by_owner(&self, id: usize) -> Result<Vec<AppData>> {
|
||||
pub async fn query_app_data(&self, query: AppDataQuery) -> Result<AppDataQueryResult> {
|
||||
let conn = match self.0.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = query_rows!(
|
||||
&conn,
|
||||
"SELECT * FROM app_data WHERE owner = $1 ORDER BY created DESC",
|
||||
&[&(id as i64)],
|
||||
|x| { Self::get_app_data_from_row(x) }
|
||||
let query_str = query.to_string().replace(
|
||||
"%q%",
|
||||
&match query.query {
|
||||
AppDataSelectQuery::Like(_, _) => format!("v LIKE $1"),
|
||||
},
|
||||
);
|
||||
|
||||
if res.is_err() {
|
||||
return Err(Error::GeneralNotFound("app_data".to_string()));
|
||||
}
|
||||
let res = match query.mode {
|
||||
AppDataSelectMode::One => AppDataQueryResult::One(
|
||||
match query_row!(&conn, &query_str, params![&query.query.to_string()], |x| {
|
||||
Ok(Self::get_app_data_from_row(x))
|
||||
}) {
|
||||
Ok(x) => x,
|
||||
Err(_) => return Err(Error::GeneralNotFound("app_data".to_string())),
|
||||
},
|
||||
),
|
||||
AppDataSelectMode::Many(_, _, _) => AppDataQueryResult::Many(
|
||||
match query_rows!(&conn, &query_str, params![&query.query.to_string()], |x| {
|
||||
Self::get_app_data_from_row(x)
|
||||
}) {
|
||||
Ok(x) => x,
|
||||
Err(_) => return Err(Error::GeneralNotFound("app_data".to_string())),
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
Ok(res.unwrap())
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
const MAXIMUM_FREE_APP_DATA: usize = 5;
|
||||
|
@ -114,10 +129,9 @@ impl DataManager {
|
|||
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"INSERT INTO app_data VALUES ($1, $2, $3, $4, $5)",
|
||||
"INSERT INTO app_data VALUES ($1, $2, $3, $4)",
|
||||
params![
|
||||
&(data.id as i64),
|
||||
&(data.owner as i64),
|
||||
&(data.app as i64),
|
||||
&data.key,
|
||||
&data.value
|
||||
|
@ -131,18 +145,7 @@ impl DataManager {
|
|||
Ok(data)
|
||||
}
|
||||
|
||||
pub async fn delete_app_data(&self, id: usize, user: &User) -> Result<()> {
|
||||
let app_data = self.get_app_data_by_id(id).await?;
|
||||
let app = self.get_app_by_id(app_data.app).await?;
|
||||
|
||||
// check user permission
|
||||
if ((user.id != app.owner) | (user.id != app_data.owner))
|
||||
&& !user.permissions.check(FinePermission::MANAGE_APPS)
|
||||
{
|
||||
return Err(Error::NotAllowed);
|
||||
}
|
||||
|
||||
// ...
|
||||
pub async fn delete_app_data(&self, id: usize) -> Result<()> {
|
||||
let conn = match self.0.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
|
@ -158,6 +161,6 @@ impl DataManager {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
auto_method!(update_app_data_key(&str)@get_app_data_by_id:FinePermission::MANAGE_APPS; -> "UPDATE app_data SET k = $1 WHERE id = $2" --cache-key-tmpl="atto.app_data:{}");
|
||||
auto_method!(update_app_data_value(&str)@get_app_data_by_id:FinePermission::MANAGE_APPS; -> "UPDATE app_data SET v = $1 WHERE id = $2" --cache-key-tmpl="atto.app_data:{}");
|
||||
auto_method!(update_app_data_key(&str) -> "UPDATE app_data SET k = $1 WHERE id = $2" --cache-key-tmpl="atto.app_data:{}");
|
||||
auto_method!(update_app_data_value(&str) -> "UPDATE app_data SET v = $1 WHERE id = $2" --cache-key-tmpl="atto.app_data:{}");
|
||||
}
|
||||
|
|
|
@ -7,10 +7,7 @@ use crate::model::{
|
|||
Error, Result,
|
||||
};
|
||||
use crate::{auto_method, DataManager};
|
||||
|
||||
use oiseau::PostgresRow;
|
||||
|
||||
use oiseau::{execute, get, query_rows, params};
|
||||
use oiseau::{PostgresRow, execute, get, query_row, query_rows, params};
|
||||
|
||||
impl DataManager {
|
||||
/// Get a [`ThirdPartyApp`] from an SQL row.
|
||||
|
@ -26,10 +23,13 @@ impl DataManager {
|
|||
banned: get!(x->7(i32)) as i8 == 1,
|
||||
grants: get!(x->8(i32)) as usize,
|
||||
scopes: serde_json::from_str(&get!(x->9(String))).unwrap(),
|
||||
api_key: get!(x->10(String)),
|
||||
data_used: get!(x->11(i32)) as usize,
|
||||
}
|
||||
}
|
||||
|
||||
auto_method!(get_app_by_id(usize as i64)@get_app_from_row -> "SELECT * FROM apps WHERE id = $1" --name="app" --returns=ThirdPartyApp --cache-key-tmpl="atto.app:{}");
|
||||
auto_method!(get_app_by_api_key(&str)@get_app_from_row -> "SELECT * FROM apps WHERE api_key = $1" --name="app" --returns=ThirdPartyApp --cache-key-tmpl="atto.app:{}");
|
||||
|
||||
/// Get all apps by user.
|
||||
///
|
||||
|
@ -90,7 +90,7 @@ impl DataManager {
|
|||
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"INSERT INTO apps VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
|
||||
"INSERT INTO apps VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)",
|
||||
params![
|
||||
&(data.id as i64),
|
||||
&(data.created as i64),
|
||||
|
@ -102,6 +102,8 @@ impl DataManager {
|
|||
&{ if data.banned { 1 } else { 0 } },
|
||||
&(data.grants as i32),
|
||||
&serde_json::to_string(&data.scopes).unwrap(),
|
||||
&data.api_key,
|
||||
&(data.data_used as i32)
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -133,6 +135,19 @@ impl DataManager {
|
|||
}
|
||||
|
||||
self.0.1.remove(format!("atto.app:{}", id)).await;
|
||||
|
||||
// remove data
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"DELETE FROM app_data WHERE app = $1",
|
||||
&[&(id as i64)]
|
||||
);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
// ...
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -141,6 +156,7 @@ impl DataManager {
|
|||
auto_method!(update_app_redirect(&str)@get_app_by_id:FinePermission::MANAGE_APPS; -> "UPDATE apps SET redirect = $1 WHERE id = $2" --cache-key-tmpl="atto.app:{}");
|
||||
auto_method!(update_app_quota_status(AppQuota) -> "UPDATE apps SET quota_status = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.app:{}");
|
||||
auto_method!(update_app_scopes(Vec<AppScope>)@get_app_by_id:FinePermission::MANAGE_APPS; -> "UPDATE apps SET scopes = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.app:{}");
|
||||
auto_method!(update_app_api_key(&str) -> "UPDATE apps SET api_key = $1 WHERE id = $2" --cache-key-tmpl="atto.app:{}");
|
||||
|
||||
auto_method!(incr_app_grants() -> "UPDATE apps SET grants = grants + 1 WHERE id = $1" --cache-key-tmpl="atto.app:{}" --incr);
|
||||
auto_method!(decr_app_grants()@get_app_by_id -> "UPDATE apps SET grants = grants - 1 WHERE id = $1" --cache-key-tmpl="atto.app:{}" --decr=grants);
|
||||
|
|
|
@ -5,7 +5,6 @@ use crate::model::{
|
|||
communities_permissions::CommunityPermission, channels::Channel,
|
||||
};
|
||||
use crate::{auto_method, DataManager};
|
||||
|
||||
use oiseau::{PostgresRow, execute, get, query_row, query_rows, params};
|
||||
|
||||
impl DataManager {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
mod app_data;
|
||||
pub mod app_data;
|
||||
mod apps;
|
||||
mod audit_log;
|
||||
mod auth;
|
||||
|
|
|
@ -2,7 +2,10 @@ use std::fmt::Display;
|
|||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp};
|
||||
use crate::model::oauth::AppScope;
|
||||
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)]
|
||||
pub enum AppQuota {
|
||||
|
@ -83,6 +86,10 @@ pub struct ThirdPartyApp {
|
|||
///
|
||||
/// 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,
|
||||
}
|
||||
|
||||
impl ThirdPartyApp {
|
||||
|
@ -99,6 +106,8 @@ impl ThirdPartyApp {
|
|||
banned: false,
|
||||
grants: 0,
|
||||
scopes: Vec::new(),
|
||||
api_key: String::new(),
|
||||
data_used: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -106,7 +115,6 @@ impl ThirdPartyApp {
|
|||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct AppData {
|
||||
pub id: usize,
|
||||
pub owner: usize,
|
||||
pub app: usize,
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
|
@ -114,15 +122,26 @@ pub struct AppData {
|
|||
|
||||
impl AppData {
|
||||
/// Create a new [`AppData`].
|
||||
pub fn new(owner: usize, app: usize, key: String, value: String) -> Self {
|
||||
pub fn new(app: usize, key: String, value: String) -> Self {
|
||||
Self {
|
||||
id: Snowflake::new().to_string().parse::<usize>().unwrap(),
|
||||
owner,
|
||||
app,
|
||||
key,
|
||||
value,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the data limit of a given user.
|
||||
pub fn user_limit(user: &User) -> usize {
|
||||
if user
|
||||
.secondary_permissions
|
||||
.check(SecondaryPermission::DEVELOPER_PASS)
|
||||
{
|
||||
PASS_DATA_LIMIT
|
||||
} else {
|
||||
FREE_DATA_LIMIT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
|
@ -154,7 +173,8 @@ impl Display for AppDataSelectMode {
|
|||
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}"
|
||||
"ORDER BY v::jsonb->>'{order_by_top_level_key}' LIMIT {} OFFSET {offset}",
|
||||
if *limit > 1024 { 1024 } else { *limit }
|
||||
)
|
||||
}
|
||||
})
|
||||
|
@ -171,8 +191,14 @@ pub struct AppDataQuery {
|
|||
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 {}",
|
||||
"SELECT * FROM app_data WHERE app = {} AND %q% {}",
|
||||
self.app, self.mode
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum AppDataQueryResult {
|
||||
One(AppData),
|
||||
Many(Vec<AppData>),
|
||||
}
|
||||
|
|
|
@ -51,6 +51,7 @@ pub enum Error {
|
|||
QuestionsDisabled,
|
||||
RequiresSupporter,
|
||||
DrawingsDisabled,
|
||||
AppHitStorageLimit,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
|
@ -75,6 +76,7 @@ impl Display for Error {
|
|||
Self::QuestionsDisabled => "You are not allowed to ask questions there".to_string(),
|
||||
Self::RequiresSupporter => "Only site supporters can do this".to_string(),
|
||||
Self::DrawingsDisabled => "You are not allowed to submit drawings there".to_string(),
|
||||
Self::AppHitStorageLimit => "This app has already hit its storage limit, or will do so if this data is processed.".to_string(),
|
||||
_ => format!("An unknown error as occurred: ({:?})", self),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -177,6 +177,7 @@ bitflags! {
|
|||
const MANAGE_DOMAINS = 1 << 2;
|
||||
const MANAGE_SERVICES = 1 << 3;
|
||||
const MANAGE_PRODUCTS = 1 << 4;
|
||||
const DEVELOPER_PASS = 1 << 5;
|
||||
|
||||
const _ = !0;
|
||||
}
|
||||
|
|
|
@ -33,6 +33,18 @@ pub fn salt() -> String {
|
|||
.collect()
|
||||
}
|
||||
|
||||
pub fn salt_len(len: usize) -> String {
|
||||
rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(len)
|
||||
.map(char::from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn random_id() -> String {
|
||||
hash(uuid())
|
||||
}
|
||||
|
||||
pub fn random_id_salted_len(len: usize) -> String {
|
||||
hash(uuid() + &salt_len(len))
|
||||
}
|
||||
|
|
2
sql_changes/apps_api_key.sql
Normal file
2
sql_changes/apps_api_key.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE apps
|
||||
ADD COLUMN api_key TEXT NOT NULL DEFAULT '';
|
2
sql_changes/apps_data_used.sql
Normal file
2
sql_changes/apps_data_used.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE apps
|
||||
ADD COLUMN data_used INT NOT NULL DEFAULT 0;
|
Loading…
Add table
Add a link
Reference in a new issue