add: audit log, reports
add: theme preference setting
This commit is contained in:
parent
b2df2739a7
commit
d3d0c41334
38 changed files with 925 additions and 169 deletions
|
@ -54,6 +54,10 @@ pub const COMMUNITIES_SETTINGS: &str = include_str!("./public/html/communities/s
|
|||
pub const TIMELINES_HOME: &str = include_str!("./public/html/timelines/home.html");
|
||||
pub const TIMELINES_POPULAR: &str = include_str!("./public/html/timelines/popular.html");
|
||||
|
||||
pub const MOD_AUDIT_LOG: &str = include_str!("./public/html/mod/audit_log.html");
|
||||
pub const MOD_REPORTS: &str = include_str!("./public/html/mod/reports.html");
|
||||
pub const MOD_FILE_REPORT: &str = include_str!("./public/html/mod/file_report.html");
|
||||
|
||||
// langs
|
||||
pub const LANG_EN_US: &str = include_str!("./langs/en-US.toml");
|
||||
|
||||
|
@ -173,6 +177,10 @@ pub(crate) async fn write_assets(config: &Config) -> PathBufD {
|
|||
write_template!(html_path->"timelines/home.html"(crate::assets::TIMELINES_HOME) -d "timelines" --config=config);
|
||||
write_template!(html_path->"timelines/popular.html"(crate::assets::TIMELINES_POPULAR) --config=config);
|
||||
|
||||
write_template!(html_path->"mod/audit_log.html"(crate::assets::MOD_AUDIT_LOG) -d "mod" --config=config);
|
||||
write_template!(html_path->"mod/reports.html"(crate::assets::MOD_REPORTS) --config=config);
|
||||
write_template!(html_path->"mod/file_report.html"(crate::assets::MOD_FILE_REPORT) --config=config);
|
||||
|
||||
html_path
|
||||
}
|
||||
|
||||
|
|
|
@ -7,9 +7,18 @@ version = "1.0.0"
|
|||
"general:link.communities" = "Communities"
|
||||
"general:link.next" = "Next"
|
||||
"general:link.previous" = "Previous"
|
||||
"general:link.source_code" = "Source code"
|
||||
"general:link.audit_log" = "Audit log"
|
||||
"general:link.reports" = "Reports"
|
||||
"general:action.save" = "Save"
|
||||
"general:action.delete" = "Delete"
|
||||
"general:action.back" = "Back"
|
||||
"general:action.report" = "Report"
|
||||
"general:action.manage" = "Manage"
|
||||
"general:label.mod" = "Mod"
|
||||
"general:label.file_report" = "File report"
|
||||
"general:label.account_banned" = "Account banned"
|
||||
"general:label.account_banned_body" = "Your account has been banned for violating our policies."
|
||||
|
||||
"dialog:action.okay" = "Ok"
|
||||
"dialog:action.continue" = "Continue"
|
||||
|
@ -72,3 +81,5 @@ version = "1.0.0"
|
|||
"settings:label.new_username" = "New username"
|
||||
"settings:label.change_avatar" = "Change avatar"
|
||||
"settings:label.change_banner" = "Change banner"
|
||||
|
||||
"mod_panel:label.open_reported_content" = "Open reported content"
|
||||
|
|
|
@ -63,7 +63,13 @@ macro_rules! get_user_from_token {
|
|||
))
|
||||
.await
|
||||
{
|
||||
Ok(ua) => Some(ua),
|
||||
Ok(ua) => {
|
||||
if ua.permissions.check_banned() {
|
||||
Some(tetratto_core::model::auth::User::banned())
|
||||
} else {
|
||||
Some(ua)
|
||||
}
|
||||
}
|
||||
Err(_) => None,
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -15,14 +15,42 @@
|
|||
{{ components::community_avatar(id=community.id,
|
||||
community=community, size="72px") }}
|
||||
<div class="flex flex-col">
|
||||
<!-- prettier-ignore -->
|
||||
<h3 id="title" class="title">
|
||||
{% if community.context.display_name %}
|
||||
{{ community.context.display_name }}
|
||||
{% else %}
|
||||
{{ community.title }}
|
||||
{% endif %}
|
||||
</h3>
|
||||
<div class="flex gap-2 items-center">
|
||||
<h3 id="title" class="title">
|
||||
<!-- prettier-ignore -->
|
||||
{% if community.context.display_name %}
|
||||
{{ community.context.display_name }}
|
||||
{% else %}
|
||||
{{ community.title }}
|
||||
{% endif %}
|
||||
</h3>
|
||||
|
||||
{% if user %} {% if user.id != community.owner
|
||||
%}
|
||||
<div class="dropdown">
|
||||
<button
|
||||
class="camo small"
|
||||
onclick="trigger('atto::hooks::dropdown', [event])"
|
||||
exclude="dropdown"
|
||||
>
|
||||
{{ icon "ellipsis" }}
|
||||
</button>
|
||||
|
||||
<div class="inner left">
|
||||
<button
|
||||
class="red"
|
||||
onclick="trigger('me::report', ['{{ community.id }}', 'community'])"
|
||||
>
|
||||
{{ icon "flag" }}
|
||||
<span
|
||||
>{{ text "general:action.report"
|
||||
}}</span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %} {% endif %}
|
||||
</div>
|
||||
|
||||
<span class="fade">{{ community.title }}</span>
|
||||
</div>
|
||||
|
|
|
@ -227,66 +227,6 @@
|
|||
document.getElementById("uid").value = uid;
|
||||
}
|
||||
|
||||
globalThis.ban_user = async (uid) => {
|
||||
if (
|
||||
!(await trigger("atto::confirm", [
|
||||
"Are you sure you would like to do this?",
|
||||
]))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(
|
||||
`/api/v1/communities/{{ community.id }}/memberships/${uid}/role`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
role: 33,
|
||||
}),
|
||||
},
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
trigger("atto::toast", [
|
||||
res.ok ? "success" : "error",
|
||||
res.message,
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
globalThis.unban_user = async (uid) => {
|
||||
if (
|
||||
!(await trigger("atto::confirm", [
|
||||
"Are you sure you would like to do this?",
|
||||
]))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(
|
||||
`/api/v1/communities/{{ community.id }}/memberships/${uid}/role`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
role: 5,
|
||||
}),
|
||||
},
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
trigger("atto::toast", [
|
||||
res.ok ? "success" : "error",
|
||||
res.message,
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
globalThis.update_user_role = async (uid, new_role) => {
|
||||
if (
|
||||
!(await trigger("atto::confirm", [
|
||||
|
@ -356,7 +296,7 @@
|
|||
|
||||
element.innerHTML = `<div class="flex gap-2 flex-wrap" ui_ident="actions">
|
||||
<a target="_blank" class="button" href="/api/v1/auth/profile/find/${e.target.uid.value}">Open user profile</a>
|
||||
${res.payload.role !== 33 ? `<button class="red quaternary" onclick="ban_user('${e.target.uid.value}')">Ban</button>` : `<button class="quaternary" onclick="unban_user('${e.target.uid.value}')">Unban</button>`}
|
||||
${res.payload.role !== 33 ? `<button class="red quaternary" onclick="update_user_role('${e.target.uid.value}', 33)">Ban</button>` : `<button class="quaternary" onclick="update_user_role('${e.target.uid.value}', 5)">Unban</button>`}
|
||||
${res.payload.role !== 65 ? `<button class="red quaternary" onclick="update_user_role('${e.target.uid.value}', 65)">Send to review</button>` : `<button class="green quaternary" onclick="update_user_role('${e.target.uid.value}', 5)">Accept join request</button>`}
|
||||
<button class="red quaternary" onclick="kick_user('${e.target.uid.value}')">Kick</button>
|
||||
</div>`;
|
||||
|
|
|
@ -161,7 +161,7 @@ show_community=true) -%} {% if community and show_community %}
|
|||
{{ icon "external-link" }}
|
||||
</a>
|
||||
|
||||
{% if user %} {% if (user.id == post.owner) or is_helper %}
|
||||
{% if user %}
|
||||
<div class="dropdown">
|
||||
<button
|
||||
class="camo small"
|
||||
|
@ -172,6 +172,17 @@ show_community=true) -%} {% if community and show_community %}
|
|||
</button>
|
||||
|
||||
<div class="inner">
|
||||
{% if user.id != post.owner %}
|
||||
<button
|
||||
class="red"
|
||||
onclick="trigger('me::report', ['{{ post.id }}', 'post'])"
|
||||
>
|
||||
{{ icon "flag" }}
|
||||
<span>{{ text "general:action.report" }}</span>
|
||||
</button>
|
||||
{% endif %} {% if (user.id == post.owner) or is_helper
|
||||
%}
|
||||
<b class="title">{{ text "general:action.manage" }}</b>
|
||||
<button
|
||||
class="red"
|
||||
onclick="trigger('me::remove_post', ['{{ post.id }}'])"
|
||||
|
@ -179,9 +190,10 @@ show_community=true) -%} {% if community and show_community %}
|
|||
{{ icon "trash" }}
|
||||
<span>{{ text "general:action.delete" }}</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %} {% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -75,17 +75,30 @@ show_lhs=true) -%}
|
|||
<span>{{ text "auth:link.settings" }}</span>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/trisuaso/tetratto">
|
||||
{{ icon "code" }}
|
||||
<span>{{ text "general:link.source_code" }}</span>
|
||||
</a>
|
||||
|
||||
{% if is_helper %}
|
||||
<b class="title">{{ text "general:label.mod" }}</b>
|
||||
|
||||
<a href="/mod_panel/audit_log">
|
||||
{{ icon "scroll-text" }}
|
||||
<span>{{ text "general:link.audit_log" }}</span>
|
||||
</a>
|
||||
|
||||
<a href="/mod_panel/reports">
|
||||
{{ icon "flag" }}
|
||||
<span>{{ text "general:link.reports" }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<div class="title"></div>
|
||||
<button class="red" onclick="trigger('me::logout')">
|
||||
{{ icon "log-out" }}
|
||||
<span>{{ text "auth:action.logout" }}</span>
|
||||
</button>
|
||||
|
||||
<div class="title"></div>
|
||||
<a href="https://github.com/trisuaso/tetratto">
|
||||
{{ icon "code" }}
|
||||
<span>View source</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
|
|
36
crates/app/src/public/html/mod/audit_log.html
Normal file
36
crates/app/src/public/html/mod/audit_log.html
Normal file
|
@ -0,0 +1,36 @@
|
|||
{% import "macros.html" as macros %} {% extends "root.html" %} {% block head %}
|
||||
<title>Audit log - {{ config.name }}</title>
|
||||
{% endblock %} {% block body %} {{ macros::nav(selected="notifications") }}
|
||||
<main class="flex flex-col gap-2">
|
||||
<div class="card-nest w-full">
|
||||
<div class="card small flex items-center gap-2">
|
||||
{{ icon "scroll" }}
|
||||
<span>{{ text "general:link.audit_log" }}</span>
|
||||
</div>
|
||||
|
||||
<div class="card flex flex-col gap-2">
|
||||
<!-- prettier-ignore -->
|
||||
{% for item in items %}
|
||||
<div class="card-nest">
|
||||
<a
|
||||
class="card small flex items-center gap-2 flush"
|
||||
href="/api/v1/auth/profile/find/{{ item.moderator }}"
|
||||
>
|
||||
<!-- prettier-ignore -->
|
||||
{{ components::avatar(username=item.moderator, selector_type="id") }}
|
||||
<span>{{ item.moderator }}</span>
|
||||
<span class="fade date">{{ item.created }}</span>
|
||||
</a>
|
||||
|
||||
<div class="card secondary">
|
||||
<span>{{ item.content|markdown|safe }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
{{ components::pagination(page=page, items=items|length) }}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
65
crates/app/src/public/html/mod/file_report.html
Normal file
65
crates/app/src/public/html/mod/file_report.html
Normal file
|
@ -0,0 +1,65 @@
|
|||
{% import "macros.html" as macros %} {% extends "root.html" %} {% block head %}
|
||||
<title>File report - {{ config.name }}</title>
|
||||
{% endblock %} {% block body %} {{ macros::nav(selected="notifications") }}
|
||||
<main class="flex flex-col gap-2">
|
||||
<div class="card-nest w-full">
|
||||
<div class="card small flex items-center gap-2">
|
||||
{{ icon "flag" }}
|
||||
<span>{{ text "general:label.file_report" }}</span>
|
||||
</div>
|
||||
|
||||
<form
|
||||
class="card flex flex-col gap-2"
|
||||
onsubmit="create_report_from_form(event)"
|
||||
>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="title"
|
||||
>{{ text "communities:label.content" }}</label
|
||||
>
|
||||
<textarea
|
||||
type="text"
|
||||
name="content"
|
||||
id="content"
|
||||
placeholder="content"
|
||||
required
|
||||
minlength="16"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button class="primary">
|
||||
{{ text "communities:action.create" }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
function create_report_from_form(e) {
|
||||
e.preventDefault();
|
||||
fetch("/api/v1/reports", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: e.target.content.value,
|
||||
asset: "{{ asset }}",
|
||||
asset_type: `{{ asset_type }}`,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
trigger("atto::toast", [
|
||||
res.ok ? "success" : "error",
|
||||
res.message,
|
||||
]);
|
||||
|
||||
if (res.ok) {
|
||||
setTimeout(() => {
|
||||
window.close();
|
||||
}, 150);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
79
crates/app/src/public/html/mod/reports.html
Normal file
79
crates/app/src/public/html/mod/reports.html
Normal file
|
@ -0,0 +1,79 @@
|
|||
{% import "macros.html" as macros %} {% extends "root.html" %} {% block head %}
|
||||
<title>Reports - {{ config.name }}</title>
|
||||
{% endblock %} {% block body %} {{ macros::nav(selected="notifications") }}
|
||||
<main class="flex flex-col gap-2">
|
||||
<div class="card-nest w-full">
|
||||
<div class="card small flex items-center gap-2">
|
||||
{{ icon "flag" }}
|
||||
<span>{{ text "general:link.reports" }}</span>
|
||||
</div>
|
||||
|
||||
<div class="card flex flex-col gap-2">
|
||||
<!-- prettier-ignore -->
|
||||
{% for item in items %}
|
||||
<div class="card-nest">
|
||||
<a
|
||||
class="card small flex items-center gap-2 flush"
|
||||
href="/api/v1/auth/profile/find/{{ item.owner }}"
|
||||
>
|
||||
<!-- prettier-ignore -->
|
||||
{{ components::avatar(username=item.owner, selector_type="id") }}
|
||||
<span>{{ item.owner }}</span>
|
||||
<span class="fade date">{{ item.created }}</span>
|
||||
</a>
|
||||
|
||||
<div class="card secondary flex flex-col gap-2">
|
||||
<span>{{ item.content|markdown|safe }}</span>
|
||||
|
||||
<div class="card w-full flex flex-wrap gap-2">
|
||||
<button
|
||||
onclick="open_reported_content('{{ item.asset }}', '{{ item.asset_type }}')"
|
||||
>
|
||||
{{ icon "external-link" }}
|
||||
<span
|
||||
>{{ text "mod_panel:label.open_reported_content"
|
||||
}}</span
|
||||
>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onclick="remove_report('{{ item.id }}')"
|
||||
class="red quaternary"
|
||||
>
|
||||
{{ icon "trash" }}
|
||||
<span>{{ text "general:action.delete" }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
{{ components::pagination(page=page, items=items|length) }}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
function open_reported_content(asset, asset_type) {
|
||||
if (asset_type === "Post") {
|
||||
window.open(`/post/${asset}`);
|
||||
} else if (asset_type === "Community") {
|
||||
window.open(`/community/${asset}`);
|
||||
}
|
||||
}
|
||||
|
||||
function remove_report(id) {
|
||||
fetch(`/api/v1/reports/${id}`, {
|
||||
method: "DELETE",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
trigger("atto::toast", [
|
||||
res.ok ? "success" : "error",
|
||||
res.message,
|
||||
]);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -209,6 +209,22 @@
|
|||
}}</span
|
||||
>
|
||||
</button>
|
||||
|
||||
{% if profile.permissions != 131073 %}
|
||||
<button
|
||||
class="red quaternary"
|
||||
onclick="update_user_role(131073)"
|
||||
>
|
||||
Ban
|
||||
</button>
|
||||
{% else %}
|
||||
<button
|
||||
class="quaternary"
|
||||
onclick="update_user_role(1)"
|
||||
>
|
||||
Unban
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -286,25 +302,77 @@
|
|||
});
|
||||
};
|
||||
|
||||
ui.refresh_container(element, ["actions"]);
|
||||
ui.generate_settings_ui(
|
||||
element,
|
||||
[
|
||||
[
|
||||
["is_verified", "Is verified"],
|
||||
"{{ profile.is_verified }}",
|
||||
"checkbox",
|
||||
],
|
||||
],
|
||||
null,
|
||||
{
|
||||
is_verified: (value) => {
|
||||
profile_request(false, "verified", {
|
||||
is_verified: value,
|
||||
});
|
||||
globalThis.update_user_role = async (
|
||||
new_role,
|
||||
) => {
|
||||
if (
|
||||
!(await trigger("atto::confirm", [
|
||||
"Are you sure you would like to do this?",
|
||||
]))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(
|
||||
`/api/v1/auth/profile/{{ profile.id }}/role`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type":
|
||||
"application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
role: Number.parseInt(new_role),
|
||||
}),
|
||||
},
|
||||
},
|
||||
);
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
trigger("atto::toast", [
|
||||
res.ok ? "success" : "error",
|
||||
res.message,
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
ui.refresh_container(element, ["actions"]);
|
||||
|
||||
setTimeout(() => {
|
||||
ui.refresh_container(element, ["actions"]);
|
||||
|
||||
ui.generate_settings_ui(
|
||||
element,
|
||||
[
|
||||
[
|
||||
["is_verified", "Is verified"],
|
||||
"{{ profile.is_verified }}",
|
||||
"checkbox",
|
||||
],
|
||||
[
|
||||
["role", "Permission level"],
|
||||
"{{ profile.permissions }}",
|
||||
"input",
|
||||
],
|
||||
],
|
||||
null,
|
||||
{
|
||||
is_verified: (value) => {
|
||||
profile_request(
|
||||
false,
|
||||
"verified",
|
||||
{
|
||||
is_verified: value,
|
||||
},
|
||||
);
|
||||
},
|
||||
role: (new_role) => {
|
||||
return update_user_role(
|
||||
new_role,
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
}, 100);
|
||||
}, 150);
|
||||
</script>
|
||||
</div>
|
||||
|
|
|
@ -142,6 +142,37 @@
|
|||
|
||||
<div class="w-full hidden flex flex-col gap-2" data-tab="profile">
|
||||
<div class="card tertiary flex flex-col gap-2" id="profile_settings">
|
||||
<div class="card-nest" ui_ident="theme_preference">
|
||||
<div class="card small">
|
||||
<b>Theme preference</b>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<select
|
||||
onchange="set_setting_field('theme_preference', event.target.selectedOptions[0].value)"
|
||||
>
|
||||
<option
|
||||
value="Auto"
|
||||
selected="{% if user.settings.theme_preference == 'Auto' %}true{% else %}false{% endif %}"
|
||||
>
|
||||
Auto
|
||||
</option>
|
||||
<option
|
||||
value="Light"
|
||||
selected="{% if user.settings.theme_preference == 'Light' %}true{% else %}false{% endif %}"
|
||||
>
|
||||
Light
|
||||
</option>
|
||||
<option
|
||||
value="Dark"
|
||||
selected="{% if user.settings.theme_preference == 'Dark' %}true{% else %}false{% endif %}"
|
||||
>
|
||||
Dark
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-nest" ui_ident="change_avatar">
|
||||
<div class="card small">
|
||||
<b>{{ text "settings:label.change_avatar" }}</b>
|
||||
|
@ -421,6 +452,7 @@
|
|||
"change_username",
|
||||
]);
|
||||
ui.refresh_container(profile_settings, [
|
||||
"theme_preference",
|
||||
"change_avatar",
|
||||
"change_banner",
|
||||
]);
|
||||
|
|
|
@ -14,6 +14,15 @@
|
|||
|
||||
<link rel="stylesheet" href="/css/style.css" />
|
||||
|
||||
{% if user %}
|
||||
<script>
|
||||
window.localStorage.setItem(
|
||||
"tetratto:theme",
|
||||
"{{ user.settings.theme_preference }}",
|
||||
);
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
<script src="/js/loader.js"></script>
|
||||
<script defer async src="/js/atto.js"></script>
|
||||
|
||||
|
@ -57,7 +66,28 @@
|
|||
<div id="toast_zone"></div>
|
||||
|
||||
<div id="page" style="display: contents">
|
||||
{% block body %}{% endblock %}
|
||||
<!-- prettier-ignore -->
|
||||
{% if user and user.id == 0 %}
|
||||
<article>
|
||||
<main>
|
||||
<div class="card-nest">
|
||||
<div class="card small flex items-center gap-2 red">
|
||||
{{ icon "frown" }}
|
||||
<span
|
||||
>{{ text "general:label.account_banned" }}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<span
|
||||
>{{ text "general:label.account_banned_body"
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</article>
|
||||
{% else %} {% block body %}{% endblock %} {% endif %}
|
||||
</div>
|
||||
|
||||
<script data-turbo-permanent="true" id="init-script">
|
||||
|
|
|
@ -6,20 +6,22 @@ function media_theme_pref() {
|
|||
|
||||
if (
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches &&
|
||||
!window.localStorage.getItem("tetratto:theme")
|
||||
(!window.localStorage.getItem("tetratto:theme") ||
|
||||
window.localStorage.getItem("tetratto:theme") === "Auto")
|
||||
) {
|
||||
document.documentElement.classList.add("dark");
|
||||
// window.localStorage.setItem("theme", "dark");
|
||||
} else if (
|
||||
window.matchMedia("(prefers-color-scheme: light)").matches &&
|
||||
!window.localStorage.getItem("tetratto:theme")
|
||||
(!window.localStorage.getItem("tetratto:theme") ||
|
||||
window.localStorage.getItem("tetratto:theme") === "Auto")
|
||||
) {
|
||||
document.documentElement.classList.remove("dark");
|
||||
// window.localStorage.setItem("theme", "light");
|
||||
} else if (window.localStorage.getItem("tetratto:theme")) {
|
||||
/* restore theme */
|
||||
const current = window.localStorage.getItem("tetratto:theme");
|
||||
document.documentElement.className = current;
|
||||
document.documentElement.className = current.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -144,4 +144,10 @@
|
|||
]);
|
||||
});
|
||||
});
|
||||
|
||||
self.define("report", (_, asset, asset_type) => {
|
||||
window.open(
|
||||
`/mod_panel/file_report?asset=${asset}&asset_type=${asset_type}`,
|
||||
);
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -37,7 +37,7 @@ pub async fn create_request(
|
|||
/// Delete the given IP ban.
|
||||
pub async fn delete_request(
|
||||
jar: CookieJar,
|
||||
Path(id): Path<usize>,
|
||||
Path(ip): Path<String>,
|
||||
Extension(data): Extension<State>,
|
||||
) -> impl IntoResponse {
|
||||
let data = &(data.read().await).0;
|
||||
|
@ -50,7 +50,7 @@ pub async fn delete_request(
|
|||
return Json(Error::NotAllowed.into());
|
||||
}
|
||||
|
||||
match data.delete_ipban(id, user).await {
|
||||
match data.delete_ipban(&ip, user).await {
|
||||
Ok(_) => Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "IP ban deleted".to_string(),
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
use crate::{
|
||||
State, get_user_from_token,
|
||||
model::{ApiReturn, Error},
|
||||
routes::api::v1::{DeleteUser, UpdateUserIsVerified, UpdateUserPassword, UpdateUserUsername},
|
||||
routes::api::v1::{
|
||||
DeleteUser, UpdateUserIsVerified, UpdateUserPassword, UpdateUserRole, UpdateUserUsername,
|
||||
},
|
||||
};
|
||||
use axum::{
|
||||
Extension, Json,
|
||||
|
@ -171,6 +173,29 @@ pub async fn update_user_is_verified_request(
|
|||
}
|
||||
}
|
||||
|
||||
/// Update the role of the given user.
|
||||
pub async fn update_user_role_request(
|
||||
jar: CookieJar,
|
||||
Path(id): Path<usize>,
|
||||
Extension(data): Extension<State>,
|
||||
Json(req): Json<UpdateUserRole>,
|
||||
) -> 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()),
|
||||
};
|
||||
|
||||
match data.update_user_role(id, req.role, user).await {
|
||||
Ok(_) => Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "User updated".to_string(),
|
||||
payload: (),
|
||||
}),
|
||||
Err(e) => Json(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete the given user.
|
||||
pub async fn delete_user_request(
|
||||
jar: CookieJar,
|
||||
|
|
|
@ -2,6 +2,7 @@ pub mod auth;
|
|||
pub mod communities;
|
||||
pub mod notifications;
|
||||
pub mod reactions;
|
||||
pub mod reports;
|
||||
pub mod util;
|
||||
|
||||
use axum::{
|
||||
|
@ -15,6 +16,7 @@ use tetratto_core::model::{
|
|||
PostContext,
|
||||
},
|
||||
communities_permissions::CommunityPermission,
|
||||
permissions::FinePermission,
|
||||
reactions::AssetType,
|
||||
};
|
||||
|
||||
|
@ -121,6 +123,10 @@ pub fn routes() -> Router {
|
|||
"/auth/profile/{id}/settings",
|
||||
post(auth::profile::update_user_settings_request),
|
||||
)
|
||||
.route(
|
||||
"/auth/profile/{id}/role",
|
||||
post(auth::profile::update_user_role_request),
|
||||
)
|
||||
.route(
|
||||
"/auth/profile/{id}",
|
||||
delete(auth::profile::delete_user_request),
|
||||
|
@ -175,6 +181,9 @@ pub fn routes() -> Router {
|
|||
// ipbans
|
||||
.route("/bans/{ip}", post(auth::ipbans::create_request))
|
||||
.route("/bans/id/{id}", delete(auth::ipbans::delete_request))
|
||||
// reports
|
||||
.route("/reports", post(reports::create_request))
|
||||
.route("/reports/{id}", delete(reports::delete_request))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
@ -238,6 +247,13 @@ pub struct CreateReaction {
|
|||
pub is_like: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreateReport {
|
||||
pub content: String,
|
||||
pub asset: String,
|
||||
pub asset_type: AssetType,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateUserPassword {
|
||||
pub from: String,
|
||||
|
@ -264,6 +280,11 @@ pub struct UpdateMembershipRole {
|
|||
pub role: CommunityPermission,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateUserRole {
|
||||
pub role: FinePermission,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct DeleteUser {
|
||||
pub password: String,
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
use super::UpdateNotificationRead;
|
||||
use crate::{State, get_user_from_token};
|
||||
use axum::{Extension, Json, extract::Path, response::IntoResponse};
|
||||
use axum_extra::extract::CookieJar;
|
||||
use tetratto_core::model::{ApiReturn, Error};
|
||||
|
||||
use crate::{State, get_user_from_token};
|
||||
|
||||
use super::UpdateNotificationRead;
|
||||
|
||||
pub async fn delete_request(
|
||||
jar: CookieJar,
|
||||
Extension(data): Extension<State>,
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
use crate::{State, get_user_from_token, routes::api::v1::CreateReaction};
|
||||
use axum::{Extension, Json, extract::Path, response::IntoResponse};
|
||||
use axum_extra::extract::CookieJar;
|
||||
use tetratto_core::model::{ApiReturn, Error, reactions::Reaction};
|
||||
|
||||
use crate::{State, get_user_from_token, routes::api::v1::CreateReaction};
|
||||
|
||||
pub async fn get_request(
|
||||
jar: CookieJar,
|
||||
Extension(data): Extension<State>,
|
||||
|
|
55
crates/app/src/routes/api/v1/reports.rs
Normal file
55
crates/app/src/routes/api/v1/reports.rs
Normal file
|
@ -0,0 +1,55 @@
|
|||
use super::CreateReport;
|
||||
use crate::{State, get_user_from_token};
|
||||
use axum::{Extension, Json, extract::Path, response::IntoResponse};
|
||||
use axum_extra::extract::CookieJar;
|
||||
use tetratto_core::model::{ApiReturn, Error, moderation::Report};
|
||||
|
||||
pub async fn create_request(
|
||||
jar: CookieJar,
|
||||
Extension(data): Extension<State>,
|
||||
Json(req): Json<CreateReport>,
|
||||
) -> 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 asset_id = match req.asset.parse::<usize>() {
|
||||
Ok(n) => n,
|
||||
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
|
||||
};
|
||||
|
||||
match data
|
||||
.create_report(Report::new(user.id, req.content, asset_id, req.asset_type))
|
||||
.await
|
||||
{
|
||||
Ok(_) => Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "Report created".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;
|
||||
let user = match get_user_from_token!(jar, data) {
|
||||
Some(ua) => ua,
|
||||
None => return Json(Error::NotAllowed.into()),
|
||||
};
|
||||
|
||||
match data.delete_report(id, user).await {
|
||||
Ok(_) => Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "Report deleted".to_string(),
|
||||
payload: (),
|
||||
}),
|
||||
Err(e) => Json(e.into()),
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
pub mod auth;
|
||||
pub mod communities;
|
||||
pub mod misc;
|
||||
pub mod mod_panel;
|
||||
pub mod profile;
|
||||
|
||||
use axum::{Router, routing::get};
|
||||
|
@ -20,6 +21,13 @@ pub fn routes() -> Router {
|
|||
.route("/popular", get(misc::popular_request))
|
||||
.route("/notifs", get(misc::notifications_request))
|
||||
.fallback_service(get(misc::not_found))
|
||||
// mod
|
||||
.route("/mod_panel/audit_log", get(mod_panel::audit_log_request))
|
||||
.route("/mod_panel/reports", get(mod_panel::reports_request))
|
||||
.route(
|
||||
"/mod_panel/file_report",
|
||||
get(mod_panel::file_report_request),
|
||||
)
|
||||
// auth
|
||||
.route("/auth/register", get(auth::register_request))
|
||||
.route("/auth/login", get(auth::login_request))
|
||||
|
|
115
crates/app/src/routes/pages/mod_panel.rs
Normal file
115
crates/app/src/routes/pages/mod_panel.rs
Normal file
|
@ -0,0 +1,115 @@
|
|||
use super::{PaginatedQuery, render_error};
|
||||
use crate::{State, assets::initial_context, get_lang, get_user_from_token};
|
||||
use axum::{
|
||||
Extension,
|
||||
extract::Query,
|
||||
response::{Html, IntoResponse},
|
||||
};
|
||||
use axum_extra::extract::CookieJar;
|
||||
use serde::Deserialize;
|
||||
use tetratto_core::model::{Error, permissions::FinePermission, reactions::AssetType};
|
||||
|
||||
/// `/mod_panel/audit_log`
|
||||
pub async fn audit_log_request(
|
||||
jar: CookieJar,
|
||||
Extension(data): Extension<State>,
|
||||
Query(req): Query<PaginatedQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let data = data.read().await;
|
||||
let user = match get_user_from_token!(jar, data.0) {
|
||||
Some(ua) => ua,
|
||||
None => {
|
||||
return Err(Html(
|
||||
render_error(Error::NotAllowed, &jar, &data, &None).await,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
if !user.permissions.check(FinePermission::VIEW_AUDIT_LOG) {
|
||||
return Err(Html(
|
||||
render_error(Error::NotAllowed, &jar, &data, &None).await,
|
||||
));
|
||||
}
|
||||
|
||||
let items = match data.0.get_audit_log_entries(12, req.page).await {
|
||||
Ok(p) => p,
|
||||
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
|
||||
};
|
||||
|
||||
let lang = get_lang!(jar, data.0);
|
||||
let mut context = initial_context(&data.0.0, lang, &Some(user)).await;
|
||||
context.insert("items", &items);
|
||||
context.insert("page", &req.page);
|
||||
|
||||
// return
|
||||
Ok(Html(data.1.render("mod/audit_log.html", &context).unwrap()))
|
||||
}
|
||||
|
||||
/// `/mod_panel/reports`
|
||||
pub async fn reports_request(
|
||||
jar: CookieJar,
|
||||
Extension(data): Extension<State>,
|
||||
Query(req): Query<PaginatedQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let data = data.read().await;
|
||||
let user = match get_user_from_token!(jar, data.0) {
|
||||
Some(ua) => ua,
|
||||
None => {
|
||||
return Err(Html(
|
||||
render_error(Error::NotAllowed, &jar, &data, &None).await,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
if !user.permissions.check(FinePermission::VIEW_REPORTS) {
|
||||
return Err(Html(
|
||||
render_error(Error::NotAllowed, &jar, &data, &None).await,
|
||||
));
|
||||
}
|
||||
|
||||
let items = match data.0.get_reports(12, req.page).await {
|
||||
Ok(p) => p,
|
||||
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
|
||||
};
|
||||
|
||||
let lang = get_lang!(jar, data.0);
|
||||
let mut context = initial_context(&data.0.0, lang, &Some(user)).await;
|
||||
context.insert("items", &items);
|
||||
context.insert("page", &req.page);
|
||||
|
||||
// return
|
||||
Ok(Html(data.1.render("mod/reports.html", &context).unwrap()))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct FileReportQuery {
|
||||
pub asset: String,
|
||||
pub asset_type: AssetType,
|
||||
}
|
||||
|
||||
/// `/mod_panel/file_report`
|
||||
pub async fn file_report_request(
|
||||
jar: CookieJar,
|
||||
Extension(data): Extension<State>,
|
||||
Query(req): Query<FileReportQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let data = data.read().await;
|
||||
let user = match get_user_from_token!(jar, data.0) {
|
||||
Some(ua) => ua,
|
||||
None => {
|
||||
return Err(Html(
|
||||
render_error(Error::NotAllowed, &jar, &data, &None).await,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let lang = get_lang!(jar, data.0);
|
||||
let mut context = initial_context(&data.0.0, lang, &Some(user)).await;
|
||||
context.insert("asset", &req.asset);
|
||||
context.insert("asset_type", &req.asset_type);
|
||||
|
||||
// return
|
||||
Ok(Html(
|
||||
data.1.render("mod/file_report.html", &context).unwrap(),
|
||||
))
|
||||
}
|
|
@ -3,7 +3,7 @@ use crate::cache::Cache;
|
|||
use crate::model::{
|
||||
Error, Result, auth::User, moderation::AuditLogEntry, permissions::FinePermission,
|
||||
};
|
||||
use crate::{auto_method, execute, get, query_row};
|
||||
use crate::{auto_method, execute, get, query_row, query_rows};
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
use rusqlite::Row;
|
||||
|
@ -13,7 +13,7 @@ use tokio_postgres::Row;
|
|||
|
||||
impl DataManager {
|
||||
/// Get an [`AuditLogEntry`] from an SQL row.
|
||||
pub(crate) fn get_auditlog_entry_from_row(
|
||||
pub(crate) fn get_audit_log_entry_from_row(
|
||||
#[cfg(feature = "sqlite")] x: &Row<'_>,
|
||||
#[cfg(feature = "postgres")] x: &Row,
|
||||
) -> AuditLogEntry {
|
||||
|
@ -25,13 +25,42 @@ impl DataManager {
|
|||
}
|
||||
}
|
||||
|
||||
auto_method!(get_auditlog_entry_by_id(usize)@get_auditlog_entry_from_row -> "SELECT * FROM auditlog WHERE id = $1" --name="audit log entry" --returns=AuditLogEntry --cache-key-tmpl="atto.auditlog:{}");
|
||||
auto_method!(get_audit_log_entry_by_id(usize)@get_audit_log_entry_from_row -> "SELECT * FROM audit_log WHERE id = $1" --name="audit log entry" --returns=AuditLogEntry --cache-key-tmpl="atto.audit_log:{}");
|
||||
|
||||
/// Get all audit log entries (paginated).
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `batch` - the limit of items in each page
|
||||
/// * `page` - the page number
|
||||
pub async fn get_audit_log_entries(
|
||||
&self,
|
||||
batch: usize,
|
||||
page: usize,
|
||||
) -> Result<Vec<AuditLogEntry>> {
|
||||
let conn = match self.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = query_rows!(
|
||||
&conn,
|
||||
"SELECT * FROM audit_log ORDER BY created DESC LIMIT $1 OFFSET $2",
|
||||
&[&(batch as isize), &((page * batch) as isize)],
|
||||
|x| { Self::get_audit_log_entry_from_row(x) }
|
||||
);
|
||||
|
||||
if res.is_err() {
|
||||
return Err(Error::GeneralNotFound("audit log entry".to_string()));
|
||||
}
|
||||
|
||||
Ok(res.unwrap())
|
||||
}
|
||||
|
||||
/// Create a new audit log entry in the database.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `data` - a mock [`AuditLogEntry`] object to insert
|
||||
pub async fn create_auditlog_entry(&self, data: AuditLogEntry) -> Result<()> {
|
||||
pub async fn create_audit_log_entry(&self, data: AuditLogEntry) -> Result<()> {
|
||||
let conn = match self.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
|
@ -39,7 +68,7 @@ impl DataManager {
|
|||
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"INSERT INTO auditlog VALUES ($1, $2, $3, $4)",
|
||||
"INSERT INTO audit_log VALUES ($1, $2, $3, $4)",
|
||||
&[
|
||||
&data.id.to_string().as_str(),
|
||||
&data.created.to_string().as_str(),
|
||||
|
@ -56,7 +85,7 @@ impl DataManager {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_auditlog_entry(&self, id: usize, user: User) -> Result<()> {
|
||||
pub async fn delete_audit_log_entry(&self, id: usize, user: User) -> Result<()> {
|
||||
if !user.permissions.check(FinePermission::MANAGE_AUDITLOG) {
|
||||
return Err(Error::NotAllowed);
|
||||
}
|
||||
|
@ -68,7 +97,7 @@ impl DataManager {
|
|||
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"DELETE FROM auditlog WHERE id = $1",
|
||||
"DELETE FROM audit_log WHERE id = $1",
|
||||
&[&id.to_string()]
|
||||
);
|
||||
|
||||
|
@ -76,7 +105,7 @@ impl DataManager {
|
|||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
self.2.remove(format!("atto.auditlog:{}", id)).await;
|
||||
self.2.remove(format!("atto.audit_log:{}", id)).await;
|
||||
|
||||
// return
|
||||
Ok(())
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use super::*;
|
||||
use crate::cache::Cache;
|
||||
use crate::model::moderation::AuditLogEntry;
|
||||
use crate::model::{
|
||||
Error, Result,
|
||||
auth::{Token, User, UserSettings},
|
||||
|
@ -262,6 +263,17 @@ impl DataManager {
|
|||
|
||||
self.cache_clear_user(&other_user).await;
|
||||
|
||||
// create audit log entry
|
||||
self.create_audit_log_entry(AuditLogEntry::new(
|
||||
user.id,
|
||||
format!(
|
||||
"invoked `update_user_verified_status` with x value `{}` and y value `{}`",
|
||||
other_user.id, x
|
||||
),
|
||||
))
|
||||
.await?;
|
||||
|
||||
// ...
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -326,6 +338,67 @@ impl DataManager {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_user_role(
|
||||
&self,
|
||||
id: usize,
|
||||
role: FinePermission,
|
||||
user: User,
|
||||
) -> Result<()> {
|
||||
// check permission
|
||||
if !user.permissions.check(FinePermission::MANAGE_USERS) {
|
||||
return Err(Error::NotAllowed);
|
||||
}
|
||||
|
||||
let other_user = self.get_user_by_id(id).await?;
|
||||
|
||||
if other_user.permissions.check_manager() && !user.permissions.check_admin() {
|
||||
return Err(Error::MiscError(
|
||||
"Cannot manage the role of other managers".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if other_user.permissions == user.permissions {
|
||||
return Err(Error::MiscError(
|
||||
"Cannot manage users of equal level to you".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// ...
|
||||
let conn = match self.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"UPDATE users SET permissions = $1 WHERE id = $2",
|
||||
&[
|
||||
&(role.bits()).to_string().as_str(),
|
||||
&id.to_string().as_str()
|
||||
]
|
||||
);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
self.cache_clear_user(&other_user).await;
|
||||
|
||||
// create audit log entry
|
||||
self.create_audit_log_entry(AuditLogEntry::new(
|
||||
user.id,
|
||||
format!(
|
||||
"invoked `update_user_role` with x value `{}` and y value `{}`",
|
||||
other_user.id,
|
||||
role.bits()
|
||||
),
|
||||
))
|
||||
.await?;
|
||||
|
||||
// ...
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn cache_clear_user(&self, user: &User) {
|
||||
self.2.remove(format!("atto.user:{}", user.id)).await;
|
||||
self.2.remove(format!("atto.user:{}", user.username)).await;
|
||||
|
|
|
@ -22,7 +22,7 @@ impl DataManager {
|
|||
execute!(&conn, common::CREATE_TABLE_USERFOLLOWS).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_USERBLOCKS).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_IPBANS).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_AUDITLOG).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_AUDIT_LOG).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_REPORTS).unwrap();
|
||||
|
||||
Ok(())
|
||||
|
@ -139,7 +139,7 @@ macro_rules! auto_method {
|
|||
if !user.permissions.check(FinePermission::$permission) {
|
||||
return Err(Error::NotAllowed);
|
||||
} else {
|
||||
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
|
||||
self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new(
|
||||
user.id,
|
||||
format!("invoked `{}` with x value `{id}`", stringify!($name)),
|
||||
))
|
||||
|
@ -169,7 +169,7 @@ macro_rules! auto_method {
|
|||
if !user.permissions.check(FinePermission::$permission) {
|
||||
return Err(Error::NotAllowed);
|
||||
} else {
|
||||
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
|
||||
self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new(
|
||||
user.id,
|
||||
format!("invoked `{}` with x value `{id}`", stringify!($name)),
|
||||
))
|
||||
|
@ -201,7 +201,7 @@ macro_rules! auto_method {
|
|||
if !user.permissions.check(FinePermission::$permission) {
|
||||
return Err(Error::NotAllowed);
|
||||
} else {
|
||||
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
|
||||
self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new(
|
||||
user.id,
|
||||
format!("invoked `{}` with x value `{id}`", stringify!($name)),
|
||||
))
|
||||
|
@ -232,7 +232,7 @@ macro_rules! auto_method {
|
|||
if !user.permissions.check(FinePermission::$permission) {
|
||||
return Err(Error::NotAllowed);
|
||||
} else {
|
||||
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
|
||||
self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new(
|
||||
user.id,
|
||||
format!("invoked `{}` with x value `{x}`", stringify!($name)),
|
||||
))
|
||||
|
@ -265,7 +265,7 @@ macro_rules! auto_method {
|
|||
if !user.permissions.check(FinePermission::$permission) {
|
||||
return Err(Error::NotAllowed);
|
||||
} else {
|
||||
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
|
||||
self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new(
|
||||
user.id,
|
||||
format!("invoked `{}` with x value `{id}`", stringify!($name), id),
|
||||
))
|
||||
|
@ -300,7 +300,7 @@ macro_rules! auto_method {
|
|||
if !user.permissions.check(FinePermission::$permission) {
|
||||
return Err(Error::NotAllowed);
|
||||
} else {
|
||||
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
|
||||
self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new(
|
||||
user.id,
|
||||
format!("invoked `{}` with x value `{x:?}`", stringify!($name)),
|
||||
))
|
||||
|
@ -455,7 +455,7 @@ macro_rules! auto_method {
|
|||
if !user.permissions.check(FinePermission::$permission) {
|
||||
return Err(Error::NotAllowed);
|
||||
} else {
|
||||
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
|
||||
self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new(
|
||||
user.id,
|
||||
format!("invoked `{}` with x value `{id}`", stringify!($name)),
|
||||
))
|
||||
|
@ -488,7 +488,7 @@ macro_rules! auto_method {
|
|||
if !user.permissions.check(FinePermission::$permission) {
|
||||
return Err(Error::NotAllowed);
|
||||
} else {
|
||||
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
|
||||
self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new(
|
||||
user.id,
|
||||
format!("invoked `{}` with x value `{x}`", stringify!($name)),
|
||||
))
|
||||
|
@ -546,7 +546,7 @@ macro_rules! auto_method {
|
|||
if !user.permissions.check(FinePermission::$permission) {
|
||||
return Err(Error::NotAllowed);
|
||||
} else {
|
||||
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
|
||||
self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new(
|
||||
user.id,
|
||||
format!("invoked `{}` with x value `{x:?}`", stringify!($name)),
|
||||
))
|
||||
|
|
|
@ -224,7 +224,7 @@ impl DataManager {
|
|||
if !user.permissions.check(FinePermission::MANAGE_COMMUNITIES) {
|
||||
return Err(Error::NotAllowed);
|
||||
} else {
|
||||
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
|
||||
self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new(
|
||||
user.id,
|
||||
format!("invoked `delete_community` with x value `{id}`"),
|
||||
))
|
||||
|
|
|
@ -7,5 +7,5 @@ pub const CREATE_TABLE_NOTIFICATIONS: &str = include_str!("./sql/create_notifica
|
|||
pub const CREATE_TABLE_USERFOLLOWS: &str = include_str!("./sql/create_userfollows.sql");
|
||||
pub const CREATE_TABLE_USERBLOCKS: &str = include_str!("./sql/create_userblocks.sql");
|
||||
pub const CREATE_TABLE_IPBANS: &str = include_str!("./sql/create_ipbans.sql");
|
||||
pub const CREATE_TABLE_AUDITLOG: &str = include_str!("./sql/create_auditlog.sql");
|
||||
pub const CREATE_TABLE_AUDIT_LOG: &str = include_str!("./sql/create_audit_log.sql");
|
||||
pub const CREATE_TABLE_REPORTS: &str = include_str!("./sql/create_reports.sql");
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
created INTEGER NOT NULL,
|
||||
owner INTEGER NOT NULL,
|
||||
content TEXT NOT NULL
|
||||
)
|
|
@ -1,6 +0,0 @@
|
|||
CREATE TABLE IF NOT EXISTS auditlog (
|
||||
ip TEXT NOT NULL,
|
||||
created INTEGER NOT NULL PRIMARY KEY,
|
||||
moderator TEXT NOT NULL,
|
||||
content TEXT NOT NULL
|
||||
)
|
|
@ -1,7 +1,7 @@
|
|||
CREATE TABLE IF NOT EXISTS reports (
|
||||
ip TEXT NOT NULL,
|
||||
created INTEGER NOT NULL PRIMARY KEY,
|
||||
owner TEXT NOT NULL,
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
created INTEGER NOT NULL,
|
||||
owner INTEGER NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
asset INTEGER NOT NULL,
|
||||
asset_type TEXT NOT NULL
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use super::*;
|
||||
use crate::cache::Cache;
|
||||
use crate::model::moderation::AuditLogEntry;
|
||||
use crate::model::{Error, Result, auth::IpBan, auth::User, permissions::FinePermission};
|
||||
use crate::{auto_method, execute, get, query_row};
|
||||
|
||||
|
@ -57,11 +58,18 @@ impl DataManager {
|
|||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
// create audit log entry
|
||||
self.create_audit_log_entry(AuditLogEntry::new(
|
||||
user.id,
|
||||
format!("invoked `create_ipban` with x value `{}`", data.ip),
|
||||
))
|
||||
.await?;
|
||||
|
||||
// return
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_ipban(&self, id: usize, user: User) -> Result<()> {
|
||||
pub async fn delete_ipban(&self, ip: &str, user: User) -> Result<()> {
|
||||
// ONLY moderators can manage ip bans
|
||||
if !user.permissions.check(FinePermission::MANAGE_BANS) {
|
||||
return Err(Error::NotAllowed);
|
||||
|
@ -72,17 +80,20 @@ impl DataManager {
|
|||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"DELETE FROM ipbans WHERE id = $1",
|
||||
&[&id.to_string()]
|
||||
);
|
||||
let res = execute!(&conn, "DELETE FROM ipbans WHERE ip = $1", &[ip]);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
self.2.remove(format!("atto.ipban:{}", id)).await;
|
||||
self.2.remove(format!("atto.ipban:{}", ip)).await;
|
||||
|
||||
// create audit log entry
|
||||
self.create_audit_log_entry(AuditLogEntry::new(
|
||||
user.id,
|
||||
format!("invoked `delete_ipban` with x value `{ip}`"),
|
||||
))
|
||||
.await?;
|
||||
|
||||
// return
|
||||
Ok(())
|
||||
|
|
|
@ -331,6 +331,31 @@ impl DataManager {
|
|||
);
|
||||
}
|
||||
|
||||
// incr comment count
|
||||
if let Some(id) = data.replying_to {
|
||||
self.incr_post_comments(id).await.unwrap();
|
||||
|
||||
// send notification
|
||||
let rt = self.get_post_by_id(id).await?;
|
||||
|
||||
if data.owner != rt.owner {
|
||||
let owner = self.get_user_by_id(rt.owner).await?;
|
||||
self.create_notification(Notification::new(
|
||||
"Your post has received a new comment!".to_string(),
|
||||
format!(
|
||||
"[@{}](/api/v1/auth/profile/find/{}) has commented on your [post](/post/{}).",
|
||||
owner.username, owner.id, rt.id
|
||||
),
|
||||
rt.owner,
|
||||
))
|
||||
.await?;
|
||||
|
||||
if rt.context.comments_enabled == false {
|
||||
return Err(Error::NotAllowed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
let conn = match self.connect().await {
|
||||
Ok(c) => c,
|
||||
|
@ -364,27 +389,6 @@ impl DataManager {
|
|||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
// incr comment count
|
||||
if let Some(id) = data.replying_to {
|
||||
self.incr_post_comments(id).await.unwrap();
|
||||
|
||||
// send notification
|
||||
let rt = self.get_post_by_id(id).await?;
|
||||
|
||||
if data.owner != rt.owner {
|
||||
let owner = self.get_user_by_id(rt.owner).await?;
|
||||
self.create_notification(Notification::new(
|
||||
"Your post has received a new comment!".to_string(),
|
||||
format!(
|
||||
"[@{}](/api/v1/auth/profile/find/{}) has commented on your [post](/post/{}).",
|
||||
owner.username, owner.id, rt.id
|
||||
),
|
||||
rt.owner,
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
// return
|
||||
Ok(data.id)
|
||||
}
|
||||
|
@ -396,7 +400,7 @@ impl DataManager {
|
|||
if !user.permissions.check(FinePermission::MANAGE_POSTS) {
|
||||
return Err(Error::NotAllowed);
|
||||
} else {
|
||||
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
|
||||
self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new(
|
||||
user.id,
|
||||
format!("invoked `delete_post` with x value `{id}`"),
|
||||
))
|
||||
|
|
|
@ -144,6 +144,9 @@ impl DataManager {
|
|||
}
|
||||
}
|
||||
}
|
||||
AssetType::User => {
|
||||
return Err(Error::NotAllowed);
|
||||
}
|
||||
};
|
||||
|
||||
// return
|
||||
|
@ -200,6 +203,9 @@ impl DataManager {
|
|||
return Err(e);
|
||||
}
|
||||
}
|
||||
AssetType::User => {
|
||||
return Err(Error::NotAllowed);
|
||||
}
|
||||
};
|
||||
|
||||
// return
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
use super::*;
|
||||
use crate::cache::Cache;
|
||||
use crate::model::moderation::AuditLogEntry;
|
||||
use crate::model::{Error, Result, auth::User, moderation::Report, permissions::FinePermission};
|
||||
use crate::{auto_method, execute, get, query_row};
|
||||
use crate::{auto_method, execute, get, query_row, query_rows};
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
use rusqlite::Row;
|
||||
|
@ -27,6 +28,31 @@ impl DataManager {
|
|||
|
||||
auto_method!(get_report_by_id(usize)@get_report_from_row -> "SELECT * FROM reports WHERE id = $1" --name="report" --returns=Report --cache-key-tmpl="atto.reports:{}");
|
||||
|
||||
/// Get all reports (paginated).
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `batch` - the limit of items in each page
|
||||
/// * `page` - the page number
|
||||
pub async fn get_reports(&self, batch: usize, page: usize) -> Result<Vec<Report>> {
|
||||
let conn = match self.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = query_rows!(
|
||||
&conn,
|
||||
"SELECT * FROM reports ORDER BY created DESC LIMIT $1 OFFSET $2",
|
||||
&[&(batch as isize), &((page * batch) as isize)],
|
||||
|x| { Self::get_report_from_row(x) }
|
||||
);
|
||||
|
||||
if res.is_err() {
|
||||
return Err(Error::GeneralNotFound("report".to_string()));
|
||||
}
|
||||
|
||||
Ok(res.unwrap())
|
||||
}
|
||||
|
||||
/// Create a new report in the database.
|
||||
///
|
||||
/// # Arguments
|
||||
|
@ -80,6 +106,13 @@ impl DataManager {
|
|||
|
||||
self.2.remove(format!("atto.report:{}", id)).await;
|
||||
|
||||
// create audit log entry
|
||||
self.create_audit_log_entry(AuditLogEntry::new(
|
||||
user.id,
|
||||
format!("invoked `delete_report` with x value `{id}`"),
|
||||
))
|
||||
.await?;
|
||||
|
||||
// return
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -25,6 +25,19 @@ pub struct User {
|
|||
pub following_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum ThemePreference {
|
||||
Auto,
|
||||
Dark,
|
||||
Light,
|
||||
}
|
||||
|
||||
impl Default for ThemePreference {
|
||||
fn default() -> Self {
|
||||
Self::Auto
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct UserSettings {
|
||||
#[serde(default)]
|
||||
|
@ -35,6 +48,8 @@ pub struct UserSettings {
|
|||
pub private_profile: bool,
|
||||
#[serde(default)]
|
||||
pub private_communities: bool,
|
||||
#[serde(default)]
|
||||
pub theme_preference: ThemePreference,
|
||||
}
|
||||
|
||||
impl Default for UserSettings {
|
||||
|
@ -44,6 +59,7 @@ impl Default for UserSettings {
|
|||
biography: String::new(),
|
||||
private_profile: false,
|
||||
private_communities: false,
|
||||
theme_preference: ThemePreference::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -88,6 +104,15 @@ impl User {
|
|||
}
|
||||
}
|
||||
|
||||
/// Banned user profile.
|
||||
pub fn banned() -> Self {
|
||||
Self {
|
||||
username: "<banned>".to_string(),
|
||||
id: 0,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new token
|
||||
///
|
||||
/// # Returns
|
||||
|
|
|
@ -25,6 +25,7 @@ bitflags! {
|
|||
const MANAGE_VERIFIED = 1 << 14;
|
||||
const MANAGE_AUDITLOG = 1 << 15;
|
||||
const MANAGE_REPORTS = 1 << 16;
|
||||
const BANNED = 1 << 17;
|
||||
|
||||
const _ = !0;
|
||||
}
|
||||
|
@ -101,6 +102,9 @@ impl FinePermission {
|
|||
if (self & FinePermission::ADMINISTRATOR) == FinePermission::ADMINISTRATOR {
|
||||
// has administrator permission, meaning everything else is automatically true
|
||||
return true;
|
||||
} else if self.check_banned() {
|
||||
// has banned permission, meaning everything else is automatically false
|
||||
return false;
|
||||
}
|
||||
|
||||
(self & permission) == permission
|
||||
|
@ -118,7 +122,17 @@ impl FinePermission {
|
|||
|
||||
/// Check if the given [`FinePermission`] qualifies as "Manager" status.
|
||||
pub fn check_manager(self) -> bool {
|
||||
self.check_helper() && self.check(FinePermission::ADMINISTRATOR)
|
||||
self.check_helper() && self.check(FinePermission::MANAGE_USERS)
|
||||
}
|
||||
|
||||
/// Check if the given [`FinePermission`] qualifies as "Administrator" status.
|
||||
pub fn check_admin(self) -> bool {
|
||||
self.check_manager() && self.check(FinePermission::ADMINISTRATOR)
|
||||
}
|
||||
|
||||
/// Check if the given [`FinePermission`] qualifies as "Banned" status.
|
||||
pub fn check_banned(self) -> bool {
|
||||
(self & FinePermission::BANNED) == FinePermission::BANNED
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,8 +4,12 @@ use tetratto_shared::{snow::AlmostSnowflake, unix_epoch_timestamp};
|
|||
/// All of the items which support reactions.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum AssetType {
|
||||
#[serde(alias = "community")]
|
||||
Community,
|
||||
#[serde(alias = "post")]
|
||||
Post,
|
||||
#[serde(alias = "user")]
|
||||
User,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue