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.manage_scopes" = "Manage scopes"
|
||||||
"developer:label.scopes" = "Scopes"
|
"developer:label.scopes" = "Scopes"
|
||||||
"developer:label.guides_and_help" = "Guides & help"
|
"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.delete" = "Delete app"
|
||||||
"developer:action.authorize" = "Authorize"
|
"developer:action.authorize" = "Authorize"
|
||||||
|
|
||||||
|
|
|
@ -419,3 +419,20 @@ macro_rules! ignore_users_gen {
|
||||||
.concat()
|
.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 {
|
.poll_bar {
|
||||||
background-color: var(--color-primary);
|
background-color: var(--color-primary);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
height: 25px;
|
height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.poll_option {
|
.poll_option {
|
||||||
|
@ -413,6 +413,22 @@ select:focus {
|
||||||
overflow-wrap: anywhere;
|
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"] {
|
input[type="checkbox"] {
|
||||||
--color: #c9b1bc;
|
--color: #c9b1bc;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
|
|
|
@ -159,7 +159,6 @@
|
||||||
(text "{{ icon \"notepad-text-dashed\" }}"))
|
(text "{{ icon \"notepad-text-dashed\" }}"))
|
||||||
(text "{%- endif %} {%- endif %}")
|
(text "{%- endif %} {%- endif %}")
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
(text "{{ text \"communities:action.create\" }}"))))))
|
(text "{{ text \"communities:action.create\" }}"))))))
|
||||||
(text "{% if not quoting -%}")
|
(text "{% if not quoting -%}")
|
||||||
(script
|
(script
|
||||||
|
|
|
@ -29,7 +29,6 @@
|
||||||
("minlength" "2")
|
("minlength" "2")
|
||||||
("maxlength" "32")))
|
("maxlength" "32")))
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
(text "{{ text \"communities:action.create\" }}"))))
|
(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 %}")
|
(text "{% if list|length >= 4 -%} {{ components::supporter_ad(body=\"Become a supporter to create up to 10 communities!\") }} {%- endif %} {%- endif %}")
|
||||||
(div
|
(div
|
||||||
|
|
|
@ -39,7 +39,6 @@
|
||||||
("class" "flex gap-2")
|
("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 %}")
|
(text "{{ components::emoji_picker(element_id=\"content\", render_dialog=true) }} {% if is_supporter -%} {{ components::file_picker(files_list_id=\"files_list\") }} {% endif %}")
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
(text "{{ text \"requests:label.answer\" }}")))))
|
(text "{{ text \"requests:label.answer\" }}")))))
|
||||||
(text "{%- endif %}")
|
(text "{%- endif %}")
|
||||||
(div
|
(div
|
||||||
|
|
|
@ -28,7 +28,6 @@
|
||||||
("maxlength" "32")
|
("maxlength" "32")
|
||||||
("value" "{{ text }}")))
|
("value" "{{ text }}")))
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
(text "{{ text \"dialog:action.continue\" }}"))))
|
(text "{{ text \"dialog:action.continue\" }}"))))
|
||||||
(div
|
(div
|
||||||
("class" "card-nest")
|
("class" "card-nest")
|
||||||
|
|
|
@ -135,7 +135,6 @@
|
||||||
("required" "")
|
("required" "")
|
||||||
("minlength" "2")))
|
("minlength" "2")))
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
(text "{{ icon \"check\" }}")
|
(text "{{ icon \"check\" }}")
|
||||||
(span
|
(span
|
||||||
(text "{{ text \"general:action.save\" }}"))))))
|
(text "{{ text \"general:action.save\" }}"))))))
|
||||||
|
@ -190,7 +189,6 @@
|
||||||
("accept" "image/png,image/jpeg,image/avif,image/webp,image/gif")
|
("accept" "image/png,image/jpeg,image/avif,image/webp,image/gif")
|
||||||
("class" "w-content"))
|
("class" "w-content"))
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
(text "{{ icon \"check\" }}"))))
|
(text "{{ icon \"check\" }}"))))
|
||||||
(div
|
(div
|
||||||
("class" "card-nest")
|
("class" "card-nest")
|
||||||
|
@ -213,7 +211,6 @@
|
||||||
("accept" "image/png,image/jpeg,image/avif,image/webp")
|
("accept" "image/png,image/jpeg,image/avif,image/webp")
|
||||||
("class" "w-content"))
|
("class" "w-content"))
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
(text "{{ icon \"check\" }}")))
|
(text "{{ icon \"check\" }}")))
|
||||||
(span
|
(span
|
||||||
("class" "fade")
|
("class" "fade")
|
||||||
|
@ -245,7 +242,6 @@
|
||||||
("required" "")
|
("required" "")
|
||||||
("minlength" "18")))
|
("minlength" "18")))
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
(text "{{ text \"communities:action.select\" }}")))))
|
(text "{{ text \"communities:action.select\" }}")))))
|
||||||
(div
|
(div
|
||||||
("class" "card flex flex-col gap-2 w-full")
|
("class" "card flex flex-col gap-2 w-full")
|
||||||
|
@ -296,7 +292,6 @@
|
||||||
("minlength" "2")
|
("minlength" "2")
|
||||||
("maxlength" "32")))
|
("maxlength" "32")))
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
(text "{{ text \"communities:action.create\" }}"))))
|
(text "{{ text \"communities:action.create\" }}"))))
|
||||||
(text "{% for channel in channels %}")
|
(text "{% for channel in channels %}")
|
||||||
(div
|
(div
|
||||||
|
|
|
@ -779,7 +779,6 @@
|
||||||
(div
|
(div
|
||||||
("class" "flex gap-2")
|
("class" "flex gap-2")
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
(text "{{ text \"communities:action.create\" }}"))
|
(text "{{ text \"communities:action.create\" }}"))
|
||||||
|
|
||||||
(text "{% if drawing_enabled -%}")
|
(text "{% if drawing_enabled -%}")
|
||||||
|
@ -1879,7 +1878,6 @@
|
||||||
("id" "join_or_leave")
|
("id" "join_or_leave")
|
||||||
(text "{% if not is_owner -%} {% if not is_joined -%} {% if not is_pending %}")
|
(text "{% if not is_owner -%} {% if not is_joined -%} {% if not is_pending %}")
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
("onclick" "join_community()")
|
("onclick" "join_community()")
|
||||||
(text "{{ icon \"circle-plus\" }}")
|
(text "{{ icon \"circle-plus\" }}")
|
||||||
(span
|
(span
|
||||||
|
|
|
@ -10,11 +10,27 @@
|
||||||
(div
|
(div
|
||||||
("id" "manage_fields")
|
("id" "manage_fields")
|
||||||
("class" "card lowered flex flex-col gap-2")
|
("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 -%}")
|
(text "{% if is_helper -%}")
|
||||||
(div
|
(div
|
||||||
("class" "card-nest")
|
("class" "card-nest")
|
||||||
(div
|
(div
|
||||||
("class" "card small")
|
("class" "card small flex items-center gap-2")
|
||||||
|
(icon (text "infinity"))
|
||||||
(b (str (text "developer:label.change_quota_status"))))
|
(b (str (text "developer:label.change_quota_status"))))
|
||||||
(div
|
(div
|
||||||
("class" "card")
|
("class" "card")
|
||||||
|
@ -32,7 +48,8 @@
|
||||||
(div
|
(div
|
||||||
("class" "card-nest")
|
("class" "card-nest")
|
||||||
(div
|
(div
|
||||||
("class" "card small")
|
("class" "card small flex items-center gap-2")
|
||||||
|
(icon (text "pencil"))
|
||||||
(b (str (text "developer:label.change_title"))))
|
(b (str (text "developer:label.change_title"))))
|
||||||
(form
|
(form
|
||||||
("class" "card flex flex-col gap-2")
|
("class" "card flex flex-col gap-2")
|
||||||
|
@ -50,14 +67,14 @@
|
||||||
("required" "")
|
("required" "")
|
||||||
("minlength" "2")))
|
("minlength" "2")))
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
(text "{{ icon \"check\" }}")
|
(text "{{ icon \"check\" }}")
|
||||||
(span
|
(span
|
||||||
(text "{{ text \"general:action.save\" }}")))))
|
(text "{{ text \"general:action.save\" }}")))))
|
||||||
(div
|
(div
|
||||||
("class" "card-nest")
|
("class" "card-nest")
|
||||||
(div
|
(div
|
||||||
("class" "card small")
|
("class" "card small flex items-center gap-2")
|
||||||
|
(icon (text "house"))
|
||||||
(b (str (text "developer:label.change_homepage"))))
|
(b (str (text "developer:label.change_homepage"))))
|
||||||
(form
|
(form
|
||||||
("class" "card flex flex-col gap-2")
|
("class" "card flex flex-col gap-2")
|
||||||
|
@ -75,14 +92,14 @@
|
||||||
("required" "")
|
("required" "")
|
||||||
("minlength" "2")))
|
("minlength" "2")))
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
(text "{{ icon \"check\" }}")
|
(text "{{ icon \"check\" }}")
|
||||||
(span
|
(span
|
||||||
(text "{{ text \"general:action.save\" }}")))))
|
(text "{{ text \"general:action.save\" }}")))))
|
||||||
(div
|
(div
|
||||||
("class" "card-nest")
|
("class" "card-nest")
|
||||||
(div
|
(div
|
||||||
("class" "card small")
|
("class" "card small flex items-center gap-2")
|
||||||
|
(icon (text "goal"))
|
||||||
(b (str (text "developer:label.change_redirect"))))
|
(b (str (text "developer:label.change_redirect"))))
|
||||||
(form
|
(form
|
||||||
("class" "card flex flex-col gap-2")
|
("class" "card flex flex-col gap-2")
|
||||||
|
@ -100,14 +117,14 @@
|
||||||
("required" "")
|
("required" "")
|
||||||
("minlength" "2")))
|
("minlength" "2")))
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
(text "{{ icon \"check\" }}")
|
(text "{{ icon \"check\" }}")
|
||||||
(span
|
(span
|
||||||
(text "{{ text \"general:action.save\" }}")))))
|
(text "{{ text \"general:action.save\" }}")))))
|
||||||
(div
|
(div
|
||||||
("class" "card-nest")
|
("class" "card-nest")
|
||||||
(div
|
(div
|
||||||
("class" "card small")
|
("class" "card small flex items-center gap-2")
|
||||||
|
(icon (text "telescope"))
|
||||||
(b (str (text "developer:label.manage_scopes"))))
|
(b (str (text "developer:label.manage_scopes"))))
|
||||||
(form
|
(form
|
||||||
("class" "card flex flex-col gap-2")
|
("class" "card flex flex-col gap-2")
|
||||||
|
@ -140,10 +157,22 @@
|
||||||
(icon (text "external-link")) (text "Docs"))))
|
(icon (text "external-link")) (text "Docs"))))
|
||||||
|
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
(text "{{ icon \"check\" }}")
|
(text "{{ icon \"check\" }}")
|
||||||
(span
|
(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
|
(div
|
||||||
("class" "card flex flex-col gap-2")
|
("class" "card flex flex-col gap-2")
|
||||||
(ul
|
(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 () => {
|
globalThis.delete_app = async () => {
|
||||||
if (
|
if (
|
||||||
!(await trigger(\"atto::confirm\", [
|
!(await trigger(\"atto::confirm\", [
|
||||||
|
|
|
@ -57,7 +57,6 @@
|
||||||
("minlength" "2")
|
("minlength" "2")
|
||||||
("maxlength" "32")))
|
("maxlength" "32")))
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
(text "{{ text \"communities:action.create\" }}"))))
|
(text "{{ text \"communities:action.create\" }}"))))
|
||||||
|
|
||||||
; app listing
|
; app listing
|
||||||
|
|
|
@ -30,7 +30,6 @@
|
||||||
("minlength" "2")
|
("minlength" "2")
|
||||||
("maxlength" "32")))
|
("maxlength" "32")))
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
(text "{{ text \"communities:action.create\" }}"))))
|
(text "{{ text \"communities:action.create\" }}"))))
|
||||||
(text "{% else %}")
|
(text "{% else %}")
|
||||||
(text "{{ components::supporter_ad(body=\"Become a supporter to create forges!\") }}")
|
(text "{{ components::supporter_ad(body=\"Become a supporter to create forges!\") }}")
|
||||||
|
|
|
@ -253,7 +253,6 @@
|
||||||
("required" "")
|
("required" "")
|
||||||
("minlength" "2")))
|
("minlength" "2")))
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
(text "{{ icon \"check\" }}")
|
(text "{{ icon \"check\" }}")
|
||||||
(span
|
(span
|
||||||
(text "{{ text \"general:action.save\" }}")))))))
|
(text "{{ text \"general:action.save\" }}")))))))
|
||||||
|
|
|
@ -59,7 +59,6 @@
|
||||||
(option ("value" "{{ tld }}") (text ".{{ tld|lower }}"))
|
(option ("value" "{{ tld }}") (text ".{{ tld|lower }}"))
|
||||||
(text "{%- endfor %}")))
|
(text "{%- endfor %}")))
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
(text "{{ text \"communities:action.create\" }}"))
|
(text "{{ text \"communities:action.create\" }}"))
|
||||||
|
|
||||||
(details
|
(details
|
||||||
|
|
|
@ -45,7 +45,6 @@
|
||||||
("minlength" "2")
|
("minlength" "2")
|
||||||
("maxlength" "32")))
|
("maxlength" "32")))
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
(text "{{ text \"communities:action.create\" }}"))))
|
(text "{{ text \"communities:action.create\" }}"))))
|
||||||
(text "{%- endif %}")
|
(text "{%- endif %}")
|
||||||
(div
|
(div
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
(p (text "You'll find out what each achievement is when you get it, so look around!"))
|
(p (text "You'll find out what each achievement is when you get it, so look around!"))
|
||||||
(hr)
|
(hr)
|
||||||
(span (b (text "Your progress: ")) (text "{{ percentage|round(method=\"floor\", precision=2) }}%"))
|
(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
|
(div
|
||||||
("class" "card-nest")
|
("class" "card-nest")
|
||||||
|
|
|
@ -132,7 +132,6 @@
|
||||||
(text "{{ text \"auth:action.ip_block\" }}")))
|
(text "{{ text \"auth:action.ip_block\" }}")))
|
||||||
|
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
(text "{{ text \"requests:label.answer\" }}")))))
|
(text "{{ text \"requests:label.answer\" }}")))))
|
||||||
(text "{% endfor %}")))
|
(text "{% endfor %}")))
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,6 @@
|
||||||
("required" "")
|
("required" "")
|
||||||
("minlength" "16")))
|
("minlength" "16")))
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
(text "{{ text \"communities:action.create\" }}")))))
|
(text "{{ text \"communities:action.create\" }}")))))
|
||||||
|
|
||||||
(script
|
(script
|
||||||
|
|
|
@ -298,7 +298,6 @@
|
||||||
("minlength" "2")
|
("minlength" "2")
|
||||||
(text "{{ profile.ban_reason|remove_script_tags|safe }}")))
|
(text "{{ profile.ban_reason|remove_script_tags|safe }}")))
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
(str (text "general:action.save")))))
|
(str (text "general:action.save")))))
|
||||||
(div
|
(div
|
||||||
("class" "card-nest w-full")
|
("class" "card-nest w-full")
|
||||||
|
@ -396,6 +395,7 @@
|
||||||
MANAGE_DOMAINS: 1 << 2,
|
MANAGE_DOMAINS: 1 << 2,
|
||||||
MANAGE_SERVICES: 1 << 3,
|
MANAGE_SERVICES: 1 << 3,
|
||||||
MANAGE_PRODUCTS: 1 << 4,
|
MANAGE_PRODUCTS: 1 << 4,
|
||||||
|
DEVELOPER_PASS: 1 << 5,
|
||||||
},
|
},
|
||||||
\"secondary_role\",
|
\"secondary_role\",
|
||||||
\"add_permission_to_secondary_role\",
|
\"add_permission_to_secondary_role\",
|
||||||
|
|
|
@ -37,7 +37,6 @@
|
||||||
("minlength" "2")
|
("minlength" "2")
|
||||||
("maxlength" "4096")))
|
("maxlength" "4096")))
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
(text "{{ text \"communities:action.create\" }}"))))
|
(text "{{ text \"communities:action.create\" }}"))))
|
||||||
(div
|
(div
|
||||||
("class" "card-nest")
|
("class" "card-nest")
|
||||||
|
|
|
@ -80,7 +80,6 @@
|
||||||
("class" "flex gap-2")
|
("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 %}")
|
(text "{{ components::emoji_picker(element_id=\"content\", render_dialog=true) }} {% if is_supporter -%} {{ components::file_picker(files_list_id=\"files_list\") }} {% endif %}")
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
(text "{{ text \"communities:action.create\" }}")))))
|
(text "{{ text \"communities:action.create\" }}")))))
|
||||||
(text "{%- endif %}")
|
(text "{%- endif %}")
|
||||||
(div
|
(div
|
||||||
|
@ -279,7 +278,6 @@
|
||||||
("class" "flex gap-2")
|
("class" "flex gap-2")
|
||||||
(text "{{ components::emoji_picker(element_id=\"new_content\", render_dialog=false) }}")
|
(text "{{ components::emoji_picker(element_id=\"new_content\", render_dialog=false) }}")
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
(text "{{ text \"general:action.save\" }}")))))
|
(text "{{ text \"general:action.save\" }}")))))
|
||||||
(script
|
(script
|
||||||
(text "async function edit_post_from_form(e) {
|
(text "async function edit_post_from_form(e) {
|
||||||
|
|
|
@ -276,7 +276,6 @@
|
||||||
("required" "")
|
("required" "")
|
||||||
("minlength" "2")))
|
("minlength" "2")))
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
(text "{{ icon \"check\" }}")
|
(text "{{ icon \"check\" }}")
|
||||||
(span
|
(span
|
||||||
(text "{{ text \"general:action.save\" }}"))))))
|
(text "{{ text \"general:action.save\" }}"))))))
|
||||||
|
@ -305,7 +304,6 @@
|
||||||
("minlength" "6")
|
("minlength" "6")
|
||||||
("autocomplete" "off")))
|
("autocomplete" "off")))
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
(text "{{ icon \"trash\" }}")
|
(text "{{ icon \"trash\" }}")
|
||||||
(span
|
(span
|
||||||
(text "{{ text \"general:action.delete\" }}")))))
|
(text "{{ text \"general:action.delete\" }}")))))
|
||||||
|
@ -419,7 +417,6 @@
|
||||||
("minlength" "6")
|
("minlength" "6")
|
||||||
("autocomplete" "off")))
|
("autocomplete" "off")))
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
(text "{{ icon \"check\" }}")
|
(text "{{ icon \"check\" }}")
|
||||||
(span
|
(span
|
||||||
(text "{{ text \"general:action.save\" }}")))))))))
|
(text "{{ text \"general:action.save\" }}")))))))))
|
||||||
|
@ -908,7 +905,6 @@
|
||||||
("accept" "image/png,image/jpeg,image/avif,image/webp,image/gif")
|
("accept" "image/png,image/jpeg,image/avif,image/webp,image/gif")
|
||||||
("class" "w-content"))
|
("class" "w-content"))
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
(text "{{ icon \"check\" }}")))
|
(text "{{ icon \"check\" }}")))
|
||||||
(span
|
(span
|
||||||
("class" "fade")
|
("class" "fade")
|
||||||
|
@ -936,7 +932,6 @@
|
||||||
("accept" "image/png,image/jpeg,image/avif,image/webp,image/gif")
|
("accept" "image/png,image/jpeg,image/avif,image/webp,image/gif")
|
||||||
("class" "w-content"))
|
("class" "w-content"))
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
(text "{{ icon \"check\" }}")))
|
(text "{{ icon \"check\" }}")))
|
||||||
(span
|
(span
|
||||||
("class" "fade")
|
("class" "fade")
|
||||||
|
@ -1054,7 +1049,6 @@
|
||||||
("class" "card w-full flex flex-wrap gap-2")
|
("class" "card w-full flex flex-wrap gap-2")
|
||||||
("ui_ident" "import_export")
|
("ui_ident" "import_export")
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
("onclick" "import_theme_settings()")
|
("onclick" "import_theme_settings()")
|
||||||
(text "{{ icon \"upload\" }}")
|
(text "{{ icon \"upload\" }}")
|
||||||
(span
|
(span
|
||||||
|
|
|
@ -29,7 +29,6 @@
|
||||||
("minlength" "2")
|
("minlength" "2")
|
||||||
("maxlength" "32")))
|
("maxlength" "32")))
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
(text "{{ text \"communities:action.create\" }}"))))
|
(text "{{ text \"communities:action.create\" }}"))))
|
||||||
(text "{%- endif %}")
|
(text "{%- endif %}")
|
||||||
(div
|
(div
|
||||||
|
|
|
@ -114,7 +114,6 @@
|
||||||
("required" "")
|
("required" "")
|
||||||
("minlength" "2")))
|
("minlength" "2")))
|
||||||
(button
|
(button
|
||||||
("class" "primary")
|
|
||||||
(text "{{ icon \"check\" }}")
|
(text "{{ icon \"check\" }}")
|
||||||
(span
|
(span
|
||||||
(text "{{ text \"general:action.save\" }}"))))))
|
(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()),
|
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 apps;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod channels;
|
pub mod channels;
|
||||||
|
@ -19,9 +20,9 @@ use axum::{
|
||||||
routing::{any, delete, get, post, put},
|
routing::{any, delete, get, post, put},
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize};
|
||||||
use tetratto_core::model::{
|
use tetratto_core::model::{
|
||||||
apps::AppQuota,
|
apps::{AppDataSelectMode, AppDataSelectQuery, AppQuota},
|
||||||
auth::AchievementName,
|
auth::AchievementName,
|
||||||
communities::{
|
communities::{
|
||||||
CommunityContext, CommunityJoinAccess, CommunityReadAccess, CommunityWriteAccess,
|
CommunityContext, CommunityJoinAccess, CommunityReadAccess, CommunityWriteAccess,
|
||||||
|
@ -32,7 +33,7 @@ use tetratto_core::model::{
|
||||||
littleweb::{DomainData, DomainTld, ServiceFsEntry},
|
littleweb::{DomainData, DomainTld, ServiceFsEntry},
|
||||||
oauth::AppScope,
|
oauth::AppScope,
|
||||||
permissions::{FinePermission, SecondaryPermission},
|
permissions::{FinePermission, SecondaryPermission},
|
||||||
products::{ProductType, ProductPrice},
|
products::{ProductPrice, ProductType},
|
||||||
reactions::AssetType,
|
reactions::AssetType,
|
||||||
stacks::{StackMode, StackPrivacy, StackSort},
|
stacks::{StackMode, StackPrivacy, StackSort},
|
||||||
};
|
};
|
||||||
|
@ -419,6 +420,7 @@ pub fn routes() -> Router {
|
||||||
)
|
)
|
||||||
// apps
|
// apps
|
||||||
.route("/apps", post(apps::create_request))
|
.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}/title", post(apps::update_title_request))
|
||||||
.route("/apps/{id}/homepage", post(apps::update_homepage_request))
|
.route("/apps/{id}/homepage", post(apps::update_homepage_request))
|
||||||
.route("/apps/{id}/redirect", post(apps::update_redirect_request))
|
.route("/apps/{id}/redirect", post(apps::update_redirect_request))
|
||||||
|
@ -427,8 +429,13 @@ pub fn routes() -> Router {
|
||||||
post(apps::update_quota_status_request),
|
post(apps::update_quota_status_request),
|
||||||
)
|
)
|
||||||
.route("/apps/{id}/scopes", post(apps::update_scopes_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}/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
|
// warnings
|
||||||
.route("/warnings/{id}", get(auth::user_warnings::get_request))
|
.route("/warnings/{id}", get(auth::user_warnings::get_request))
|
||||||
.route("/warnings/{id}", post(auth::user_warnings::create_request))
|
.route("/warnings/{id}", post(auth::user_warnings::create_request))
|
||||||
|
@ -1148,3 +1155,20 @@ pub struct UpdateProductPrice {
|
||||||
pub struct UpdateUploadAlt {
|
pub struct UpdateUploadAlt {
|
||||||
pub alt: String,
|
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,
|
Extension,
|
||||||
};
|
};
|
||||||
use axum_extra::extract::CookieJar;
|
use axum_extra::extract::CookieJar;
|
||||||
use tetratto_core::model::{permissions::FinePermission, Error};
|
use tetratto_core::model::{apps::AppData, permissions::FinePermission, Error};
|
||||||
|
|
||||||
/// `/developer`
|
/// `/developer`
|
||||||
pub async fn home_request(jar: CookieJar, Extension(data): Extension<State>) -> impl IntoResponse {
|
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 lang = get_lang!(jar, data.0);
|
||||||
let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await;
|
let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await;
|
||||||
|
|
||||||
context.insert("app", &app);
|
context.insert("app", &app);
|
||||||
|
context.insert("data_limit", &data_limit);
|
||||||
|
|
||||||
// return
|
// return
|
||||||
Ok(Html(data.1.render("developer/app.html", &context).unwrap()))
|
Ok(Html(data.1.render("developer/app.html", &context).unwrap()))
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
use oiseau::cache::Cache;
|
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 crate::{auto_method, DataManager};
|
||||||
|
use oiseau::{PostgresRow, execute, get, query_row, query_rows, params};
|
||||||
|
|
||||||
use oiseau::PostgresRow;
|
pub const FREE_DATA_LIMIT: usize = 512_000;
|
||||||
|
pub const PASS_DATA_LIMIT: usize = 5_242_880;
|
||||||
use oiseau::{execute, get, query_rows, params};
|
|
||||||
|
|
||||||
impl DataManager {
|
impl DataManager {
|
||||||
/// Get a [`AppData`] from an SQL row.
|
/// Get a [`AppData`] from an SQL row.
|
||||||
pub(crate) fn get_app_data_from_row(x: &PostgresRow) -> AppData {
|
pub(crate) fn get_app_data_from_row(x: &PostgresRow) -> AppData {
|
||||||
AppData {
|
AppData {
|
||||||
id: get!(x->0(i64)) as usize,
|
id: get!(x->0(i64)) as usize,
|
||||||
owner: get!(x->1(i64)) as usize,
|
|
||||||
app: get!(x->2(i64)) as usize,
|
app: get!(x->2(i64)) as usize,
|
||||||
key: get!(x->3(String)),
|
key: get!(x->3(String)),
|
||||||
value: get!(x->4(String)),
|
value: get!(x->4(String)),
|
||||||
|
@ -48,24 +48,39 @@ impl DataManager {
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// * `id` - the ID of the user to fetch app_data for
|
/// * `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 {
|
let conn = match self.0.connect().await {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||||
};
|
};
|
||||||
|
|
||||||
let res = query_rows!(
|
let query_str = query.to_string().replace(
|
||||||
&conn,
|
"%q%",
|
||||||
"SELECT * FROM app_data WHERE owner = $1 ORDER BY created DESC",
|
&match query.query {
|
||||||
&[&(id as i64)],
|
AppDataSelectQuery::Like(_, _) => format!("v LIKE $1"),
|
||||||
|x| { Self::get_app_data_from_row(x) }
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if res.is_err() {
|
let res = match query.mode {
|
||||||
return Err(Error::GeneralNotFound("app_data".to_string()));
|
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;
|
const MAXIMUM_FREE_APP_DATA: usize = 5;
|
||||||
|
@ -114,10 +129,9 @@ impl DataManager {
|
||||||
|
|
||||||
let res = execute!(
|
let res = execute!(
|
||||||
&conn,
|
&conn,
|
||||||
"INSERT INTO app_data VALUES ($1, $2, $3, $4, $5)",
|
"INSERT INTO app_data VALUES ($1, $2, $3, $4)",
|
||||||
params![
|
params![
|
||||||
&(data.id as i64),
|
&(data.id as i64),
|
||||||
&(data.owner as i64),
|
|
||||||
&(data.app as i64),
|
&(data.app as i64),
|
||||||
&data.key,
|
&data.key,
|
||||||
&data.value
|
&data.value
|
||||||
|
@ -131,18 +145,7 @@ impl DataManager {
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_app_data(&self, id: usize, user: &User) -> Result<()> {
|
pub async fn delete_app_data(&self, id: usize) -> 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ...
|
|
||||||
let conn = match self.0.connect().await {
|
let conn = match self.0.connect().await {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||||
|
@ -158,6 +161,6 @@ impl DataManager {
|
||||||
Ok(())
|
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_key(&str) -> "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_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,
|
Error, Result,
|
||||||
};
|
};
|
||||||
use crate::{auto_method, DataManager};
|
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};
|
|
||||||
|
|
||||||
impl DataManager {
|
impl DataManager {
|
||||||
/// Get a [`ThirdPartyApp`] from an SQL row.
|
/// Get a [`ThirdPartyApp`] from an SQL row.
|
||||||
|
@ -26,10 +23,13 @@ impl DataManager {
|
||||||
banned: get!(x->7(i32)) as i8 == 1,
|
banned: get!(x->7(i32)) as i8 == 1,
|
||||||
grants: get!(x->8(i32)) as usize,
|
grants: get!(x->8(i32)) as usize,
|
||||||
scopes: serde_json::from_str(&get!(x->9(String))).unwrap(),
|
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_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.
|
/// Get all apps by user.
|
||||||
///
|
///
|
||||||
|
@ -90,7 +90,7 @@ impl DataManager {
|
||||||
|
|
||||||
let res = execute!(
|
let res = execute!(
|
||||||
&conn,
|
&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![
|
params![
|
||||||
&(data.id as i64),
|
&(data.id as i64),
|
||||||
&(data.created as i64),
|
&(data.created as i64),
|
||||||
|
@ -102,6 +102,8 @@ impl DataManager {
|
||||||
&{ if data.banned { 1 } else { 0 } },
|
&{ if data.banned { 1 } else { 0 } },
|
||||||
&(data.grants as i32),
|
&(data.grants as i32),
|
||||||
&serde_json::to_string(&data.scopes).unwrap(),
|
&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;
|
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(())
|
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_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_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_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!(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);
|
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,
|
communities_permissions::CommunityPermission, channels::Channel,
|
||||||
};
|
};
|
||||||
use crate::{auto_method, DataManager};
|
use crate::{auto_method, DataManager};
|
||||||
|
|
||||||
use oiseau::{PostgresRow, execute, get, query_row, query_rows, params};
|
use oiseau::{PostgresRow, execute, get, query_row, query_rows, params};
|
||||||
|
|
||||||
impl DataManager {
|
impl DataManager {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
mod app_data;
|
pub mod app_data;
|
||||||
mod apps;
|
mod apps;
|
||||||
mod audit_log;
|
mod audit_log;
|
||||||
mod auth;
|
mod auth;
|
||||||
|
|
|
@ -2,7 +2,10 @@ use std::fmt::Display;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp};
|
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)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum AppQuota {
|
pub enum AppQuota {
|
||||||
|
@ -83,6 +86,10 @@ pub struct ThirdPartyApp {
|
||||||
///
|
///
|
||||||
/// Your app should handle informing users when scopes change.
|
/// Your app should handle informing users when scopes change.
|
||||||
pub scopes: Vec<AppScope>,
|
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 {
|
impl ThirdPartyApp {
|
||||||
|
@ -99,6 +106,8 @@ impl ThirdPartyApp {
|
||||||
banned: false,
|
banned: false,
|
||||||
grants: 0,
|
grants: 0,
|
||||||
scopes: Vec::new(),
|
scopes: Vec::new(),
|
||||||
|
api_key: String::new(),
|
||||||
|
data_used: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -106,7 +115,6 @@ impl ThirdPartyApp {
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct AppData {
|
pub struct AppData {
|
||||||
pub id: usize,
|
pub id: usize,
|
||||||
pub owner: usize,
|
|
||||||
pub app: usize,
|
pub app: usize,
|
||||||
pub key: String,
|
pub key: String,
|
||||||
pub value: String,
|
pub value: String,
|
||||||
|
@ -114,15 +122,26 @@ pub struct AppData {
|
||||||
|
|
||||||
impl AppData {
|
impl AppData {
|
||||||
/// Create a new [`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 {
|
Self {
|
||||||
id: Snowflake::new().to_string().parse::<usize>().unwrap(),
|
id: Snowflake::new().to_string().parse::<usize>().unwrap(),
|
||||||
owner,
|
|
||||||
app,
|
app,
|
||||||
key,
|
key,
|
||||||
value,
|
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)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
@ -154,7 +173,8 @@ impl Display for AppDataSelectMode {
|
||||||
Self::One => "LIMIT 1".to_string(),
|
Self::One => "LIMIT 1".to_string(),
|
||||||
Self::Many(order_by_top_level_key, limit, offset) => {
|
Self::Many(order_by_top_level_key, limit, offset) => {
|
||||||
format!(
|
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 {
|
impl Display for AppDataQuery {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.write_str(&format!(
|
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
|
self.app, self.mode
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub enum AppDataQueryResult {
|
||||||
|
One(AppData),
|
||||||
|
Many(Vec<AppData>),
|
||||||
|
}
|
||||||
|
|
|
@ -51,6 +51,7 @@ pub enum Error {
|
||||||
QuestionsDisabled,
|
QuestionsDisabled,
|
||||||
RequiresSupporter,
|
RequiresSupporter,
|
||||||
DrawingsDisabled,
|
DrawingsDisabled,
|
||||||
|
AppHitStorageLimit,
|
||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,6 +76,7 @@ impl Display for Error {
|
||||||
Self::QuestionsDisabled => "You are not allowed to ask questions there".to_string(),
|
Self::QuestionsDisabled => "You are not allowed to ask questions there".to_string(),
|
||||||
Self::RequiresSupporter => "Only site supporters can do this".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::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),
|
_ => format!("An unknown error as occurred: ({:?})", self),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -177,6 +177,7 @@ bitflags! {
|
||||||
const MANAGE_DOMAINS = 1 << 2;
|
const MANAGE_DOMAINS = 1 << 2;
|
||||||
const MANAGE_SERVICES = 1 << 3;
|
const MANAGE_SERVICES = 1 << 3;
|
||||||
const MANAGE_PRODUCTS = 1 << 4;
|
const MANAGE_PRODUCTS = 1 << 4;
|
||||||
|
const DEVELOPER_PASS = 1 << 5;
|
||||||
|
|
||||||
const _ = !0;
|
const _ = !0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,18 @@ pub fn salt() -> String {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn salt_len(len: usize) -> String {
|
||||||
|
rng()
|
||||||
|
.sample_iter(&Alphanumeric)
|
||||||
|
.take(len)
|
||||||
|
.map(char::from)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn random_id() -> String {
|
pub fn random_id() -> String {
|
||||||
hash(uuid())
|
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