add: developer panel
This commit is contained in:
parent
ebded00fd3
commit
39574df691
44 changed files with 982 additions and 84 deletions
|
@ -191,6 +191,7 @@ table ol {
|
|||
|
||||
.card.lowered {
|
||||
background: var(--color-lowered);
|
||||
color: var(--color-text-lowered);
|
||||
}
|
||||
|
||||
.card-nest {
|
||||
|
|
348
crates/app/src/public/html/developer/app.lisp
Normal file
348
crates/app/src/public/html/developer/app.lisp
Normal file
|
@ -0,0 +1,348 @@
|
|||
(text "{% extends \"root.html\" %} {% block head %}")
|
||||
(title
|
||||
(text "{{ app.title }} - {{ config.name }}"))
|
||||
|
||||
(text "{% endblock %} {% block body %} {{ macros::nav() }}")
|
||||
(main
|
||||
("class" "flex flex-col gap-2")
|
||||
(div
|
||||
("class" "w-full flex flex-col gap-2")
|
||||
(div
|
||||
("id" "manage_fields")
|
||||
("class" "card lowered flex flex-col gap-2")
|
||||
(text "{% if is_helper -%}")
|
||||
(div
|
||||
("class" "card-nest")
|
||||
(div
|
||||
("class" "card small")
|
||||
(b (str (text "developer:label.change_quota_status"))))
|
||||
(div
|
||||
("class" "card")
|
||||
(select
|
||||
("onchange" "save_quota_status(event)")
|
||||
(option
|
||||
("value" "Limited")
|
||||
("selected" "{% if app.quota_status == 'Limited' -%}true{% else %}false{%- endif %}")
|
||||
(text "Limited"))
|
||||
(option
|
||||
("value" "Unlimited")
|
||||
("selected" "{% if app.quota_status == 'Unlimited' -%}true{% else %}false{%- endif %}")
|
||||
(text "Unlimited")))))
|
||||
(text "{%- endif %}")
|
||||
(div
|
||||
("class" "card-nest")
|
||||
(div
|
||||
("class" "card small")
|
||||
(b (str (text "developer:label.change_title"))))
|
||||
(form
|
||||
("class" "card flex flex-col gap-2")
|
||||
("onsubmit" "change_title(event)")
|
||||
(div
|
||||
("class" "flex flex-col gap-1")
|
||||
(label
|
||||
("for" "title")
|
||||
(text "{{ text \"communities:label.new_title\" }}"))
|
||||
(input
|
||||
("type" "text")
|
||||
("name" "title")
|
||||
("id" "title")
|
||||
("placeholder" "new title")
|
||||
("required" "")
|
||||
("minlength" "2")))
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ icon \"check\" }}")
|
||||
(span
|
||||
(text "{{ text \"general:action.save\" }}")))))
|
||||
(div
|
||||
("class" "card-nest")
|
||||
(div
|
||||
("class" "card small")
|
||||
(b (str (text "developer:label.change_homepage"))))
|
||||
(form
|
||||
("class" "card flex flex-col gap-2")
|
||||
("onsubmit" "change_homepage(event)")
|
||||
(div
|
||||
("class" "flex flex-col gap-1")
|
||||
(label
|
||||
("for" "homepage")
|
||||
(text "{{ text \"developer:label.homepage\" }}"))
|
||||
(input
|
||||
("type" "url")
|
||||
("name" "homepage")
|
||||
("id" "homepage")
|
||||
("placeholder" "new homepage")
|
||||
("required" "")
|
||||
("minlength" "2")))
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ icon \"check\" }}")
|
||||
(span
|
||||
(text "{{ text \"general:action.save\" }}")))))
|
||||
(div
|
||||
("class" "card-nest")
|
||||
(div
|
||||
("class" "card small")
|
||||
(b (str (text "developer:label.change_redirect"))))
|
||||
(form
|
||||
("class" "card flex flex-col gap-2")
|
||||
("onsubmit" "change_redirect(event)")
|
||||
(div
|
||||
("class" "flex flex-col gap-1")
|
||||
(label
|
||||
("for" "redirect")
|
||||
(text "{{ text \"developer:label.redirect\" }}"))
|
||||
(input
|
||||
("type" "url")
|
||||
("name" "redirect")
|
||||
("id" "redirect")
|
||||
("placeholder" "new redirect URL")
|
||||
("required" "")
|
||||
("minlength" "2")))
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ icon \"check\" }}")
|
||||
(span
|
||||
(text "{{ text \"general:action.save\" }}")))))
|
||||
(div
|
||||
("class" "card-nest")
|
||||
(div
|
||||
("class" "card small")
|
||||
(b (str (text "developer:label.manage_scopes"))))
|
||||
(form
|
||||
("class" "card flex flex-col gap-2")
|
||||
("onsubmit" "change_scopes(event)")
|
||||
(div
|
||||
("class" "flex flex-col gap-1")
|
||||
(label
|
||||
("for" "scopes")
|
||||
(text "{{ text \"developer:label.scopes\" }}"))
|
||||
(input
|
||||
("type" "text")
|
||||
("name" "scopes")
|
||||
("id" "scopes")
|
||||
("placeholder" "new scopes")
|
||||
("required" "")
|
||||
("minlength" "2")
|
||||
("value" "{% for scope in app.scopes -%} {{ scope }} {% endfor %}")))
|
||||
|
||||
(pre ("class" "hidden red w-full") (code ("id" "scope_error_message") ("style" "white-space: pre-wrap")))
|
||||
|
||||
(details
|
||||
(summary ("class" "button lowered small") (icon (text "circle-help")) (text "Help"))
|
||||
(div
|
||||
("class" "card flex flex-col gap-1")
|
||||
(span ("class" "fade") (text "Scopes should be separated by a single space."))
|
||||
(a
|
||||
("class" "button")
|
||||
("href" "https://tetratto.com/reference/tetratto/model/oauth/enum.AppScope.html#variants")
|
||||
("target" "_blank")
|
||||
(icon (text "external-link")) (text "Docs"))))
|
||||
|
||||
(button
|
||||
("class" "primary")
|
||||
(text "{{ icon \"check\" }}")
|
||||
(span
|
||||
(text "{{ text \"general:action.save\" }}"))))))
|
||||
(div
|
||||
("class" "card flex flex-col gap-2")
|
||||
(ul
|
||||
(li (b (text "Homepage: ")) (text "{{ app.homepage }}"))
|
||||
(li (b (text "Redirect URL: ")) (text "{{ app.redirect }}"))
|
||||
(li (b (text "Quota status: ")) (text "{{ app.quota_status }}"))
|
||||
(li (b (text "User grants: ")) (text "{{ app.grants }}"))
|
||||
(li (b (text "Grant URL: ")) (text "{{ config.host }}/auth/connections_link/app/{{ app.id }}")))
|
||||
|
||||
(a
|
||||
("class" "button")
|
||||
("href" "https://tetratto.com/reference/tetratto/model/apps/struct.ThirdPartyApp.html#structfield.redirect")
|
||||
("target" "_blank")
|
||||
(icon (text "external-link")) (text "Docs")))
|
||||
(div
|
||||
("class" "card-nest")
|
||||
(div
|
||||
("class" "card small flex gap-1 items-center red")
|
||||
(text "{{ icon \"skull\" }}")
|
||||
(b
|
||||
(text "{{ text \"communities:label.danger_zone\" }}")))
|
||||
(div
|
||||
("class" "card flex flex-wrap gap-2")
|
||||
(button
|
||||
("class" "red lowered")
|
||||
("onclick" "delete_app()")
|
||||
(text "{{ icon \"trash\" }}")
|
||||
(span (str (text "developer:action.delete"))))))
|
||||
(div
|
||||
("class" "flex gap-2 flex-wrap")
|
||||
(a
|
||||
("href" "/developer")
|
||||
("class" "button secondary")
|
||||
(text "{{ icon \"arrow-left\" }}")
|
||||
(span
|
||||
(text "{{ text \"general:action.back\" }}"))))))
|
||||
(script
|
||||
(text "setTimeout(() => {
|
||||
globalThis.save_quota_status = (event) => {
|
||||
const selected = event.target.selectedOptions[0];
|
||||
fetch(\"/api/v1/apps/{{ app.id }}/quota_status\", {
|
||||
method: \"POST\",
|
||||
headers: {
|
||||
\"Content-Type\": \"application/json\",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
quota_status: selected.value,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
trigger(\"atto::toast\", [
|
||||
res.ok ? \"success\" : \"error\",
|
||||
res.message,
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
globalThis.change_title = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (
|
||||
!(await trigger(\"atto::confirm\", [
|
||||
\"Are you sure you would like to do this?\",
|
||||
]))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(\"/api/v1/apps/{{ app.id }}/title\", {
|
||||
method: \"POST\",
|
||||
headers: {
|
||||
\"Content-Type\": \"application/json\",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: e.target.title.value,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
trigger(\"atto::toast\", [
|
||||
res.ok ? \"success\" : \"error\",
|
||||
res.message,
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
globalThis.change_homepage = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (
|
||||
!(await trigger(\"atto::confirm\", [
|
||||
\"Are you sure you would like to do this?\",
|
||||
]))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(\"/api/v1/apps/{{ app.id }}/homepage\", {
|
||||
method: \"POST\",
|
||||
headers: {
|
||||
\"Content-Type\": \"application/json\",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
homepage: e.target.homepage.value,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
trigger(\"atto::toast\", [
|
||||
res.ok ? \"success\" : \"error\",
|
||||
res.message,
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
globalThis.change_redirect = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (
|
||||
!(await trigger(\"atto::confirm\", [
|
||||
\"Are you sure you would like to do this?\",
|
||||
]))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(\"/api/v1/apps/{{ app.id }}/redirect\", {
|
||||
method: \"POST\",
|
||||
headers: {
|
||||
\"Content-Type\": \"application/json\",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
redirect: e.target.redirect.value,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
trigger(\"atto::toast\", [
|
||||
res.ok ? \"success\" : \"error\",
|
||||
res.message,
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
globalThis.change_scopes = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (
|
||||
!(await trigger(\"atto::confirm\", [
|
||||
\"Are you sure you would like to do this? This will only impact new grants.\",
|
||||
]))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(\"/api/v1/apps/{{ app.id }}/scopes\", {
|
||||
method: \"POST\",
|
||||
headers: {
|
||||
\"Content-Type\": \"application/json\",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
scopes: e.target.scopes.value.trim().split(\" \"),
|
||||
}),
|
||||
})
|
||||
.then((res) => res.text())
|
||||
.then((res) => {
|
||||
if (res.startsWith(\"{\")) {
|
||||
const r = JSON.parse(res);
|
||||
trigger(\"atto::toast\", [r.ok ? \"success\" : \"error\", r.message]);
|
||||
document.getElementById(\"scope_error_message\").parentElement.classList.add(\"hidden\");
|
||||
} else {
|
||||
document.getElementById(\"scope_error_message\").innerText = res;
|
||||
document.getElementById(\"scope_error_message\").parentElement.classList.remove(\"hidden\");
|
||||
document.getElementById(\"scope_error_message\").parentElement.parentElement.querySelector(\"details\").setAttribute(\"open\", \"\");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
globalThis.delete_app = async () => {
|
||||
if (
|
||||
!(await trigger(\"atto::confirm\", [
|
||||
\"Are you sure you would like to do this? This action is permanent.\",
|
||||
]))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/api/v1/apps/{{ app.id }}`, {
|
||||
method: \"DELETE\",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
trigger(\"atto::toast\", [
|
||||
res.ok ? \"success\" : \"error\",
|
||||
res.message,
|
||||
]);
|
||||
});
|
||||
};
|
||||
}, 250);"))
|
||||
|
||||
(text "{% endblock %}")
|
|
@ -85,7 +85,33 @@
|
|||
("class" "date")
|
||||
(text "{{ item.created }}"))
|
||||
(text "; {{ item.quota_status }} mode; {{ item.grants }} users")))
|
||||
(text "{% endfor %}"))))
|
||||
(text "{% endfor %}")))
|
||||
|
||||
; useful links
|
||||
(div
|
||||
("class" "card-nest")
|
||||
(div
|
||||
("class" "card small flex items-center gap-2")
|
||||
(icon (text "circle-help"))
|
||||
(str (text "developer:label.guides_and_help")))
|
||||
|
||||
(div
|
||||
("class" "card")
|
||||
(ul
|
||||
(li
|
||||
(a ("href" "https://trisua.com/t/tetratto") (text "Source code")))
|
||||
(li
|
||||
(a ("href" "https://tetratto.com/reference/tetratto/index.html") (text "Source code reference")))
|
||||
(li
|
||||
(a ("href" "https://tetratto.com/reference/tetratto/model/struct.ApiReturn.html") (text "API response structure")))
|
||||
(li
|
||||
(a ("href" "https://tetratto.com/reference/tetratto/model/oauth/enum.AppScope.html") (text "App scopes")))
|
||||
(li
|
||||
(a ("href" "https://tetratto.com/reference/tetratto/model/permissions/struct.FinePermission.html") (text "User permissions")))
|
||||
(li
|
||||
(a ("href" "https://tetratto.com/reference/tetratto/model/communities_permissions/struct.CommunityPermission.html") (text "Community member permissions")))
|
||||
(li
|
||||
(a ("href" "https://tetratto.com/forge/tetratto") (text "Report issues")))))))
|
||||
|
||||
(script
|
||||
(text "async function create_app_from_form(e) {
|
||||
|
|
86
crates/app/src/public/html/developer/link.lisp
Normal file
86
crates/app/src/public/html/developer/link.lisp
Normal file
|
@ -0,0 +1,86 @@
|
|||
(text "{% extends \"root.html\" %} {% block head %}")
|
||||
(title
|
||||
(text "{{ app.title }} - {{ config.name }}"))
|
||||
|
||||
(text "{% endblock %} {% block body %} {{ macros::nav() }}")
|
||||
(main
|
||||
("class" "flex flex-col gap-2")
|
||||
(div
|
||||
("class" "w-full flex flex-col gap-2")
|
||||
(div
|
||||
("class" "card-nest")
|
||||
(div
|
||||
("class" "card flex flex-col gap-2")
|
||||
(h4 (text "Would you like to allow \"{{ app.title }}\" to access your account?"))
|
||||
(p (text "This app is requesting the following permissions on your account:"))
|
||||
|
||||
(div
|
||||
("class" "card")
|
||||
(ul
|
||||
(text "{% for scope in app.scopes -%}")
|
||||
(li
|
||||
(a
|
||||
("href" "https://tetratto.com/reference/tetratto/model/oauth/enum.AppScope.html#variant.{{ scope }}")
|
||||
("target" "_blank")
|
||||
(text "{{ scope }}")))
|
||||
(text "{%- endfor %}")))
|
||||
|
||||
(p (text "You can revoke this app's permissions at any time through your connection settings.")))
|
||||
|
||||
(div
|
||||
("class" "card flex gap-2")
|
||||
(button
|
||||
("onclick" "authorize()")
|
||||
(str (text "developer:action.authorize")))
|
||||
|
||||
(a
|
||||
("class" "button secondary")
|
||||
("href" "javascript:window.close()")
|
||||
(str (text "dialog:action.cancel")))))))
|
||||
(script
|
||||
(text "setTimeout(() => {
|
||||
globalThis.authorize = async (event) => {
|
||||
if (
|
||||
!(await trigger(\"atto::confirm\", [
|
||||
\"Are you sure you would like to do this?\",
|
||||
]))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const verifier = await trigger(\"connections::pkce_verifier\", [
|
||||
128,
|
||||
]);
|
||||
|
||||
const challenge = await trigger(\"connections::pkce_challenge\", [
|
||||
verifier,
|
||||
]);
|
||||
|
||||
fetch(\"/api/v1/apps/{{ app.id }}/grant\", {
|
||||
method: \"POST\",
|
||||
headers: {
|
||||
\"Content-Type\": \"application/json\",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
challenge
|
||||
})
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
trigger(\"atto::toast\", [
|
||||
res.ok ? \"success\" : \"error\",
|
||||
res.message,
|
||||
]);
|
||||
|
||||
if (res.ok) {
|
||||
const search = new URLSearchParams(window.location.search);
|
||||
search.append(\"verifier\", verifier);
|
||||
search.append(\"token\", res.payload);
|
||||
|
||||
window.location.href = `{{ app.redirect|remove_script_tags|safe }}?${search.toString()}`;
|
||||
}
|
||||
});
|
||||
};
|
||||
}, 250);"))
|
||||
|
||||
(text "{% endblock %}")
|
|
@ -255,6 +255,7 @@
|
|||
MANAGE_EMOJIS: 1 << 25,
|
||||
MANAGE_STACKS: 1 << 26,
|
||||
STAFF_BADGE: 1 << 27,
|
||||
MANAGE_APPS: 1 << 28,
|
||||
},
|
||||
],
|
||||
);
|
||||
|
|
|
@ -845,7 +845,57 @@
|
|||
("class" "w-content"))
|
||||
(span
|
||||
(text "Shown on profile")))))
|
||||
(text "{% endfor %}"))
|
||||
(text "{% endfor %}")
|
||||
(text "{% for grant in profile_grants %}")
|
||||
(div
|
||||
("class" "card-nest")
|
||||
(div
|
||||
("class" "card small flex items-center gap-4")
|
||||
(div
|
||||
("class" "flex items-center gap-2")
|
||||
(icon (text "bot"))
|
||||
(a
|
||||
("class" "flush")
|
||||
("href" "{{ grant[0].homepage }}")
|
||||
("target" "_blank")
|
||||
(b
|
||||
("class" "flex items-center gap-2")
|
||||
(text "{{ grant[0].title }}"))))
|
||||
|
||||
(span
|
||||
("class" "fade flex items-center gap-2")
|
||||
(icon (text "clock"))
|
||||
(span ("class" "date") (text "{{ grant[1].last_updated }}"))))
|
||||
(div
|
||||
("class" "card flex flex-col gap-2")
|
||||
(details
|
||||
(summary (icon (text "scan-eye")) (text "{{ grant[1].scopes|length }} scope{{ grant[1].scopes|length|pluralize }}"))
|
||||
|
||||
(div
|
||||
("class" "card lowered w-full")
|
||||
(ul
|
||||
(text "{% for scope in grant[1].scopes -%}")
|
||||
(li
|
||||
(a
|
||||
("href" "https://tetratto.com/reference/tetratto/model/oauth/enum.AppScope.html#variant.{{ scope }}")
|
||||
("target" "_blank")
|
||||
(text "{{ scope }}")))
|
||||
(text "{%- endfor %}"))))
|
||||
|
||||
(button
|
||||
("class" "lowered red small")
|
||||
("onclick" "remove_grant('{{ grant[0].id }}')")
|
||||
(text "{{ text \"general:action.delete\" }}"))))
|
||||
(text "{% endfor %}")
|
||||
|
||||
(hr)
|
||||
(a
|
||||
("class" "button")
|
||||
("href" "/developer")
|
||||
(icon (text "code"))
|
||||
(span
|
||||
(text "{{ config.name }} ")
|
||||
(str (text "developer:label.for_developers")))))
|
||||
(script
|
||||
("type" "application/json")
|
||||
("id" "settings_json")
|
||||
|
@ -897,6 +947,27 @@
|
|||
});
|
||||
};
|
||||
|
||||
globalThis.remove_grant = async (id) => {
|
||||
if (
|
||||
!(await trigger(\"atto::confirm\", [
|
||||
\"Are you sure you would like to do this?\",
|
||||
]))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/api/v1/auth/user/{{ profile.id }}/grants/${id}`, {
|
||||
method: \"DELETE\",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
trigger(\"atto::toast\", [
|
||||
res.ok ? \"success\" : \"error\",
|
||||
res.message,
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
globalThis.save_settings = () => {
|
||||
fetch(\"/api/v1/auth/user/{{ profile.id }}/settings\", {
|
||||
method: \"POST\",
|
||||
|
|
|
@ -818,6 +818,7 @@ media_theme_pref();
|
|||
anchor.href.length === 0 ||
|
||||
anchor.href.startsWith("https://github.com") ||
|
||||
anchor.href.startsWith("https://trisua.com") ||
|
||||
anchor.href.startsWith("https://tetratto.com") ||
|
||||
anchor.href.startsWith("https://buy.stripe.com") ||
|
||||
anchor.href.startsWith("https://billing.stripe.com") ||
|
||||
anchor.href.startsWith("https://last.fm")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue