add: ability to join/leave/be banned from communities
This commit is contained in:
parent
f3c2157dfc
commit
619184d02e
28 changed files with 618 additions and 197 deletions
|
@ -200,7 +200,7 @@ pub(crate) async fn init_dirs(config: &Config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A random ASCII value inserted into the URL of static assets to "break" the cache. Essentially just for cache busting.
|
/// A random ASCII value inserted into the URL of static assets to "break" the cache. Essentially just for cache busting.
|
||||||
pub(crate) static CACHE_BREAKER: LazyLock<String> = LazyLock::new(|| salt());
|
pub(crate) static CACHE_BREAKER: LazyLock<String> = LazyLock::new(salt);
|
||||||
|
|
||||||
/// Create the initial template context.
|
/// Create the initial template context.
|
||||||
pub(crate) async fn initial_context(
|
pub(crate) async fn initial_context(
|
||||||
|
|
|
@ -28,6 +28,7 @@ version = "1.0.0"
|
||||||
"auth:label.recent_posts" = "Recent posts"
|
"auth:label.recent_posts" = "Recent posts"
|
||||||
|
|
||||||
"communities:action.create" = "Create"
|
"communities:action.create" = "Create"
|
||||||
|
"communities:action.select" = "Select"
|
||||||
"communities:label.create_new" = "Create new community"
|
"communities:label.create_new" = "Create new community"
|
||||||
"communities:label.name" = "Name"
|
"communities:label.name" = "Name"
|
||||||
"communities:action.join" = "Join"
|
"communities:action.join" = "Join"
|
||||||
|
@ -39,6 +40,9 @@ version = "1.0.0"
|
||||||
"communities:label.create_reply" = "Create reply"
|
"communities:label.create_reply" = "Create reply"
|
||||||
"communities:label.replies" = "Replies"
|
"communities:label.replies" = "Replies"
|
||||||
"communities:action.continue_thread" = "Continue thread"
|
"communities:action.continue_thread" = "Continue thread"
|
||||||
|
"communities:tab.members" = "Members"
|
||||||
|
"communities:label.select_member" = "Select member"
|
||||||
|
"communities:label.user_id" = "User ID"
|
||||||
|
|
||||||
"notifs:action.mark_as_read" = "Mark as read"
|
"notifs:action.mark_as_read" = "Mark as read"
|
||||||
"notifs:action.mark_as_unread" = "Mark as unread"
|
"notifs:action.mark_as_unread" = "Mark as unread"
|
||||||
|
|
|
@ -7,7 +7,7 @@ macro_rules! write_template {
|
||||||
($into:ident->$path:literal($as:expr) --config=$config:ident) => {
|
($into:ident->$path:literal($as:expr) --config=$config:ident) => {
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
$into.join($path),
|
$into.join($path),
|
||||||
crate::assets::replace_in_html($as, &$config).await,
|
$crate::assets::replace_in_html($as, &$config).await,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
};
|
};
|
||||||
|
@ -29,7 +29,7 @@ macro_rules! write_template {
|
||||||
|
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
$into.join($path),
|
$into.join($path),
|
||||||
crate::assets::replace_in_html($as, &$config).await,
|
$crate::assets::replace_in_html($as, &$config).await,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,7 +17,7 @@ use tokio::sync::RwLock;
|
||||||
pub(crate) type State = Arc<RwLock<(DataManager, Tera)>>;
|
pub(crate) type State = Arc<RwLock<(DataManager, Tera)>>;
|
||||||
|
|
||||||
fn render_markdown(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
|
fn render_markdown(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
|
||||||
Ok(tetratto_shared::markdown::render_markdown(&value.as_str().unwrap()).into())
|
Ok(tetratto_shared::markdown::render_markdown(value.as_str().unwrap()).into())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
|
|
@ -29,16 +29,63 @@
|
||||||
|
|
||||||
{% if user %}
|
{% if user %}
|
||||||
<div class="card flex" id="join_or_leave">
|
<div class="card flex" id="join_or_leave">
|
||||||
{% if not is_owner %} {% if not is_member %}
|
{% if not is_owner %} {% if not is_joined %}
|
||||||
<button class="primary">
|
<button class="primary" onclick="join_community()">
|
||||||
{{ icon "circle-plus" }}
|
{{ icon "circle-plus" }}
|
||||||
<span>{{ text "communities:action.join" }}</span>
|
<span>{{ text "communities:action.join" }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
globalThis.join_community = () => {
|
||||||
|
fetch(
|
||||||
|
"/api/v1/communities/{{ community.id }}/join",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
trigger("atto::toast", [
|
||||||
|
res.ok ? "success" : "error",
|
||||||
|
res.message,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button class="camo red">
|
<button
|
||||||
|
class="quaternary red"
|
||||||
|
onclick="leave_community()"
|
||||||
|
>
|
||||||
{{ icon "circle-minus" }}
|
{{ icon "circle-minus" }}
|
||||||
<span>{{ text "communities:action.leave" }}</span>
|
<span>{{ text "communities:action.leave" }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
globalThis.leave_community = async () => {
|
||||||
|
if (
|
||||||
|
!(await trigger("atto::confirm", [
|
||||||
|
"Are you sure you would like to do this?",
|
||||||
|
]))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(
|
||||||
|
"/api/v1/communities/{{ community.id }}/memberships/{{ user.id }}",
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
trigger("atto::toast", [
|
||||||
|
res.ok ? "success" : "error",
|
||||||
|
res.message,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
{% endif %} {% else %}
|
{% endif %} {% else %}
|
||||||
<a
|
<a
|
||||||
href="/community/{{ community.title }}/manage"
|
href="/community/{{ community.title }}/manage"
|
||||||
|
@ -64,7 +111,7 @@
|
||||||
<span class="notification chip">ID</span>
|
<span class="notification chip">ID</span>
|
||||||
<button
|
<button
|
||||||
title="Copy"
|
title="Copy"
|
||||||
onclick="trigger('atto::copy_text', [{{ community.id }}])"
|
onclick="trigger('atto::copy_text', ['{{ community.id }}'])"
|
||||||
class="camo small"
|
class="camo small"
|
||||||
>
|
>
|
||||||
{{ icon "copy" }}
|
{{ icon "copy" }}
|
||||||
|
@ -76,6 +123,11 @@
|
||||||
<span class="date">{{ community.created }}</span>
|
<span class="date">{{ community.created }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full flex justify-between items-center">
|
||||||
|
<span class="notification chip">Members</span>
|
||||||
|
<span>{{ community.member_count }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="w-full flex justify-between items-center">
|
<div class="w-full flex justify-between items-center">
|
||||||
<span class="notification chip">Score</span>
|
<span class="notification chip">Score</span>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% import "macros.html" as macros %} {% import "components.html" as components
|
{% import "macros.html" as macros %} {% import "components.html" as components
|
||||||
%} {% extends "communities/base.html" %} {% block content %}
|
%} {% extends "communities/base.html" %} {% block content %}
|
||||||
<div class="flex flex-col gap-4 w-full">
|
<div class="flex flex-col gap-4 w-full">
|
||||||
{% if user %}
|
{% if user and can_post %}
|
||||||
<div class="card-nest">
|
<div class="card-nest">
|
||||||
<div class="card small">
|
<div class="card small">
|
||||||
<b>{{ text "communities:label.create_post" }}</b>
|
<b>{{ text "communities:label.create_post" }}</b>
|
||||||
|
|
|
@ -10,10 +10,14 @@
|
||||||
<a href="#/profile" data-tab-button="profile"
|
<a href="#/profile" data-tab-button="profile"
|
||||||
>{{ text "settings:tab.profile" }}</a
|
>{{ text "settings:tab.profile" }}</a
|
||||||
>
|
>
|
||||||
|
|
||||||
|
<a href="#/members" data-tab-button="members"
|
||||||
|
>{{ text "communities:tab.members" }}</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card tertiary w-full" data-tab="general">
|
<div class="w-full flex flex-col gap-2" data-tab="general">
|
||||||
<div id="manage_fields" class="flex flex-col gap-2">
|
<div id="manage_fields" class="card tertiary flex flex-col gap-2">
|
||||||
<div class="card-nest" ui_ident="read_access">
|
<div class="card-nest" ui_ident="read_access">
|
||||||
<div class="card small">
|
<div class="card small">
|
||||||
<b>Read access</b>
|
<b>Read access</b>
|
||||||
|
@ -30,7 +34,7 @@
|
||||||
|
|
||||||
<div class="card-nest" ui_ident="write_access">
|
<div class="card-nest" ui_ident="write_access">
|
||||||
<div class="card small">
|
<div class="card small">
|
||||||
<b>Write access</b>
|
<b>Post permission</b>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
@ -42,6 +46,18 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2 flex-wrap">
|
||||||
|
<button onclick="save_context()">
|
||||||
|
{{ icon "check" }}
|
||||||
|
<span>{{ text "general:action.save" }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<a href="/community/{{ community.title }}" class="button secondary">
|
||||||
|
{{ icon "arrow-left" }}
|
||||||
|
<span>{{ text "general:action.back" }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -95,19 +111,179 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2 flex-wrap">
|
<div
|
||||||
<button onclick="save_context()">
|
class="card tertiary w-full hidden flex flex-col gap-2"
|
||||||
{{ icon "check" }}
|
data-tab="members"
|
||||||
<span>{{ text "general:action.save" }}</span>
|
>
|
||||||
</button>
|
<div class="card-nest">
|
||||||
|
<div class="card small">
|
||||||
|
<b>{{ text "communities:label.select_member" }}</b>
|
||||||
|
</div>
|
||||||
|
|
||||||
<a href="/community/{{ community.title }}" class="button secondary">
|
<form
|
||||||
{{ icon "arrow-left" }}
|
class="card flex-col gap-2"
|
||||||
<span>{{ text "general:action.back" }}</span>
|
onsubmit="select_user_from_form(event)"
|
||||||
</a>
|
>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label for="uid"
|
||||||
|
>{{ text "communities:label.user_id" }}</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="uid"
|
||||||
|
id="uid"
|
||||||
|
placeholder="user id"
|
||||||
|
required
|
||||||
|
minlength="18"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="primary">
|
||||||
|
{{ text "communities:action.select" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card flex flex-col gap-2 w-full" id="membership_info"></div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
setTimeout(() => {
|
||||||
|
const element = document.getElementById("membership_info");
|
||||||
|
const ui = ns("ui");
|
||||||
|
|
||||||
|
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.select_user_from_form = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
fetch(
|
||||||
|
`/api/v1/communities/{{ community.id }}/memberships/${e.target.uid.value}`,
|
||||||
|
)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
trigger("atto::toast", [
|
||||||
|
res.ok ? "success" : "error",
|
||||||
|
res.message,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
element.innerHTML = `<div class="flex gap-2" 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>`}
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
ui.refresh_container(element, ["actions"]);
|
||||||
|
ui.generate_settings_ui(
|
||||||
|
element,
|
||||||
|
[
|
||||||
|
[
|
||||||
|
["role", "Permission level"],
|
||||||
|
res.payload.role,
|
||||||
|
"input",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
role: async (new_role) => {
|
||||||
|
if (
|
||||||
|
!(await trigger("atto::confirm", [
|
||||||
|
"Are you sure you would like to do this?",
|
||||||
|
]))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(
|
||||||
|
`/api/v1/communities/{{ community.id }}/memberships/${e.target.uid.value}/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,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, 250);
|
||||||
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const ui = ns("ui");
|
const ui = ns("ui");
|
||||||
|
|
|
@ -57,7 +57,7 @@
|
||||||
<span class="notification chip">ID</span>
|
<span class="notification chip">ID</span>
|
||||||
<button
|
<button
|
||||||
title="Copy"
|
title="Copy"
|
||||||
onclick="trigger('atto::copy_text', [{{ profile.id }}])"
|
onclick="trigger('atto::copy_text', ['{{ profile.id }}'])"
|
||||||
class="camo small"
|
class="camo small"
|
||||||
>
|
>
|
||||||
{{ icon "copy" }}
|
{{ icon "copy" }}
|
||||||
|
|
|
@ -3,34 +3,21 @@
|
||||||
{% endblock %} {% block body %} {{ macros::nav() }}
|
{% endblock %} {% block body %} {{ macros::nav() }}
|
||||||
<main class="flex flex-col gap-2">
|
<main class="flex flex-col gap-2">
|
||||||
<div class="pillmenu">
|
<div class="pillmenu">
|
||||||
<a
|
<a data-tab-button="account" class="active" href="#/account">
|
||||||
data-tab-button="account"
|
|
||||||
class="active"
|
|
||||||
href="#/account"
|
|
||||||
onclick="show_save_button()"
|
|
||||||
>
|
|
||||||
{{ text "settings:tab.account" }}
|
{{ text "settings:tab.account" }}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a
|
<a data-tab-button="profile" href="#/profile">
|
||||||
data-tab-button="profile"
|
|
||||||
href="#/profile"
|
|
||||||
onclick="show_save_button()"
|
|
||||||
>
|
|
||||||
{{ text "settings:tab.profile" }}
|
{{ text "settings:tab.profile" }}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a
|
<a data-tab-button="sessions" href="#/sessions">
|
||||||
data-tab-button="sessions"
|
|
||||||
href="#/sessions"
|
|
||||||
onclick="hide_save_button()"
|
|
||||||
>
|
|
||||||
{{ text "settings:tab.sessions" }}
|
{{ text "settings:tab.sessions" }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card w-full tertiary" data-tab="account">
|
<div class="w-full flex flex-col gap-2" data-tab="account">
|
||||||
<div class="flex flex-col gap-2" id="account_settings">
|
<div class="card tertiary flex flex-col gap-2" id="account_settings">
|
||||||
<div class="card-nest" ui_ident="change_password">
|
<div class="card-nest" ui_ident="change_password">
|
||||||
<div class="card small">
|
<div class="card small">
|
||||||
<b>{{ text "settings:label.change_password" }}</b>
|
<b>{{ text "settings:label.change_password" }}</b>
|
||||||
|
@ -107,10 +94,15 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button onclick="save_settings()" id="save_button">
|
||||||
|
{{ icon "check" }}
|
||||||
|
<span>{{ text "general:action.save" }}</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card w-full tertiary hidden" data-tab="profile">
|
<div class="w-full hidden flex flex-col gap-2" data-tab="profile">
|
||||||
<div class="flex flex-col gap-2" id="profile_settings">
|
<div class="card tertiary flex flex-col gap-2" id="profile_settings">
|
||||||
<div class="card-nest" ui_ident="change_avatar">
|
<div class="card-nest" ui_ident="change_avatar">
|
||||||
<div class="card small">
|
<div class="card small">
|
||||||
<b>{{ text "settings:label.change_avatar" }}</b>
|
<b>{{ text "settings:label.change_avatar" }}</b>
|
||||||
|
@ -188,20 +180,7 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button onclick="save_settings()" id="save_button" data-turbo-permanent>
|
|
||||||
{{ icon "check" }}
|
|
||||||
<span>{{ text "general:action.save" }}</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function show_save_button() {
|
|
||||||
document.getElementById("save_button").removeAttribute("style");
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide_save_button() {
|
|
||||||
document.getElementById("save_button").style.display = "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const ui = ns("ui");
|
const ui = ns("ui");
|
||||||
const settings = JSON.parse("{{ user_settings_serde|safe }}");
|
const settings = JSON.parse("{{ user_settings_serde|safe }}");
|
||||||
|
|
|
@ -667,7 +667,7 @@ ${option.input_element_type === "textarea" ? `${option.value}</textarea>` : ""}
|
||||||
|
|
||||||
self.define(
|
self.define(
|
||||||
"generate_settings_ui",
|
"generate_settings_ui",
|
||||||
({ $ }, into_element, options, settings_ref) => {
|
({ $ }, into_element, options, settings_ref, key_map = {}) => {
|
||||||
for (const option of options) {
|
for (const option of options) {
|
||||||
$.render_settings_ui_field(into_element, {
|
$.render_settings_ui_field(into_element, {
|
||||||
key: Array.isArray(option[0]) ? option[0][0] : option[0],
|
key: Array.isArray(option[0]) ? option[0][0] : option[0],
|
||||||
|
@ -678,7 +678,12 @@ ${option.input_element_type === "textarea" ? `${option.value}</textarea>` : ""}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.set_setting_field = (key, value) => {
|
window.set_setting_field = (key, value) => {
|
||||||
|
if (settings_ref) {
|
||||||
settings_ref[key] = value;
|
settings_ref[key] = value;
|
||||||
|
} else {
|
||||||
|
key_map[key](value);
|
||||||
|
}
|
||||||
|
|
||||||
console.log("update", key);
|
console.log("update", key);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
@ -51,13 +51,11 @@ pub async fn avatar_request(
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let data = &(data.read().await).0;
|
let data = &(data.read().await).0;
|
||||||
|
|
||||||
let user = match {
|
let user = match if req.selector_type == AvatarSelectorType::Id {
|
||||||
if req.selector_type == AvatarSelectorType::Id {
|
|
||||||
data.get_user_by_id(selector.parse::<usize>().unwrap())
|
data.get_user_by_id(selector.parse::<usize>().unwrap())
|
||||||
.await
|
.await
|
||||||
} else {
|
} else {
|
||||||
data.get_user_by_username(&selector).await
|
data.get_user_by_username(&selector).await
|
||||||
}
|
|
||||||
} {
|
} {
|
||||||
Ok(ua) => ua,
|
Ok(ua) => ua,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
|
|
|
@ -18,7 +18,7 @@ pub async fn redirect_from_id(
|
||||||
Extension(data): Extension<State>,
|
Extension(data): Extension<State>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match (&(data.read().await).0)
|
match (data.read().await).0
|
||||||
.get_user_by_id(match id.parse::<usize>() {
|
.get_user_by_id(match id.parse::<usize>() {
|
||||||
Ok(id) => id,
|
Ok(id) => id,
|
||||||
Err(_) => return Redirect::to("/"),
|
Err(_) => return Redirect::to("/"),
|
||||||
|
@ -43,11 +43,9 @@ pub async fn update_profile_settings_request(
|
||||||
None => return Json(Error::NotAllowed.into()),
|
None => return Json(Error::NotAllowed.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if user.id != id {
|
if user.id != id && !user.permissions.check(FinePermission::MANAGE_USERS) {
|
||||||
if !user.permissions.check(FinePermission::MANAGE_USERS) {
|
|
||||||
return Json(Error::NotAllowed.into());
|
return Json(Error::NotAllowed.into());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
match data.update_user_settings(id, req).await {
|
match data.update_user_settings(id, req).await {
|
||||||
Ok(_) => Json(ApiReturn {
|
Ok(_) => Json(ApiReturn {
|
||||||
|
@ -72,11 +70,9 @@ pub async fn update_profile_password_request(
|
||||||
None => return Json(Error::NotAllowed.into()),
|
None => return Json(Error::NotAllowed.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if user.id != id {
|
if user.id != id && !user.permissions.check(FinePermission::MANAGE_USERS) {
|
||||||
if !user.permissions.check(FinePermission::MANAGE_USERS) {
|
|
||||||
return Json(Error::NotAllowed.into());
|
return Json(Error::NotAllowed.into());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
match data
|
match data
|
||||||
.update_user_password(id, req.from, req.to, user, false)
|
.update_user_password(id, req.from, req.to, user, false)
|
||||||
|
@ -103,11 +99,9 @@ pub async fn update_profile_username_request(
|
||||||
None => return Json(Error::NotAllowed.into()),
|
None => return Json(Error::NotAllowed.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if user.id != id {
|
if user.id != id && !user.permissions.check(FinePermission::MANAGE_USERS) {
|
||||||
if !user.permissions.check(FinePermission::MANAGE_USERS) {
|
|
||||||
return Json(Error::NotAllowed.into());
|
return Json(Error::NotAllowed.into());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if data.get_user_by_username(&req.to).await.is_ok() {
|
if data.get_user_by_username(&req.to).await.is_ok() {
|
||||||
return Json(Error::UsernameInUse.into());
|
return Json(Error::UsernameInUse.into());
|
||||||
|
@ -136,11 +130,9 @@ pub async fn update_profile_tokens_request(
|
||||||
None => return Json(Error::NotAllowed.into()),
|
None => return Json(Error::NotAllowed.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if user.id != id {
|
if user.id != id && !user.permissions.check(FinePermission::MANAGE_USERS) {
|
||||||
if !user.permissions.check(FinePermission::MANAGE_USERS) {
|
|
||||||
return Json(Error::NotAllowed.into());
|
return Json(Error::NotAllowed.into());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
match data.update_user_tokens(id, req).await {
|
match data.update_user_tokens(id, req).await {
|
||||||
Ok(_) => Json(ApiReturn {
|
Ok(_) => Json(ApiReturn {
|
||||||
|
|
|
@ -26,7 +26,7 @@ pub async fn follow_request(
|
||||||
message: "User unfollowed".to_string(),
|
message: "User unfollowed".to_string(),
|
||||||
payload: (),
|
payload: (),
|
||||||
}),
|
}),
|
||||||
Err(e) => return Json(e.into()),
|
Err(e) => Json(e.into()),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// create
|
// create
|
||||||
|
@ -36,7 +36,7 @@ pub async fn follow_request(
|
||||||
message: "User followed".to_string(),
|
message: "User followed".to_string(),
|
||||||
payload: (),
|
payload: (),
|
||||||
}),
|
}),
|
||||||
Err(e) => return Json(e.into()),
|
Err(e) => Json(e.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,7 +61,7 @@ pub async fn block_request(
|
||||||
message: "User unblocked".to_string(),
|
message: "User unblocked".to_string(),
|
||||||
payload: (),
|
payload: (),
|
||||||
}),
|
}),
|
||||||
Err(e) => return Json(e.into()),
|
Err(e) => Json(e.into()),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// create
|
// create
|
||||||
|
@ -76,7 +76,7 @@ pub async fn block_request(
|
||||||
message: "User unfollowed".to_string(),
|
message: "User unfollowed".to_string(),
|
||||||
payload: (),
|
payload: (),
|
||||||
}),
|
}),
|
||||||
Err(e) => return Json(e.into()),
|
Err(e) => Json(e.into()),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// not following user, don't do anything else
|
// not following user, don't do anything else
|
||||||
|
@ -87,7 +87,7 @@ pub async fn block_request(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => return Json(e.into()),
|
Err(e) => Json(e.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,13 +4,18 @@ use axum::{
|
||||||
response::{IntoResponse, Redirect},
|
response::{IntoResponse, Redirect},
|
||||||
};
|
};
|
||||||
use axum_extra::extract::CookieJar;
|
use axum_extra::extract::CookieJar;
|
||||||
use tetratto_core::model::{ApiReturn, Error, communities::Community};
|
use tetratto_core::model::{
|
||||||
|
ApiReturn, Error,
|
||||||
|
auth::Notification,
|
||||||
|
communities::{Community, CommunityMembership},
|
||||||
|
communities_permissions::CommunityPermission,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
State, get_user_from_token,
|
State, get_user_from_token,
|
||||||
routes::api::v1::{
|
routes::api::v1::{
|
||||||
CreateCommunity, UpdateCommunityContext, UpdateCommunityReadAccess, UpdateCommunityTitle,
|
CreateCommunity, UpdateCommunityContext, UpdateCommunityReadAccess, UpdateCommunityTitle,
|
||||||
UpdateCommunityWriteAccess,
|
UpdateCommunityWriteAccess, UpdateMembershipRole,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -18,7 +23,8 @@ pub async fn redirect_from_id(
|
||||||
Extension(data): Extension<State>,
|
Extension(data): Extension<State>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match (&(data.read().await).0)
|
match (data.read().await)
|
||||||
|
.0
|
||||||
.get_community_by_id(match id.parse::<usize>() {
|
.get_community_by_id(match id.parse::<usize>() {
|
||||||
Ok(id) => id,
|
Ok(id) => id,
|
||||||
Err(_) => return Redirect::to("/"),
|
Err(_) => return Redirect::to("/"),
|
||||||
|
@ -50,7 +56,7 @@ pub async fn create_request(
|
||||||
message: "Community created".to_string(),
|
message: "Community created".to_string(),
|
||||||
payload: Some(id.to_string()),
|
payload: Some(id.to_string()),
|
||||||
}),
|
}),
|
||||||
Err(e) => return Json(e.into()),
|
Err(e) => Json(e.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,7 +77,7 @@ pub async fn delete_request(
|
||||||
message: "Community deleted".to_string(),
|
message: "Community deleted".to_string(),
|
||||||
payload: (),
|
payload: (),
|
||||||
}),
|
}),
|
||||||
Err(e) => return Json(e.into()),
|
Err(e) => Json(e.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,7 +99,7 @@ pub async fn update_title_request(
|
||||||
message: "Community updated".to_string(),
|
message: "Community updated".to_string(),
|
||||||
payload: (),
|
payload: (),
|
||||||
}),
|
}),
|
||||||
Err(e) => return Json(e.into()),
|
Err(e) => Json(e.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,7 +121,7 @@ pub async fn update_context_request(
|
||||||
message: "Community updated".to_string(),
|
message: "Community updated".to_string(),
|
||||||
payload: (),
|
payload: (),
|
||||||
}),
|
}),
|
||||||
Err(e) => return Json(e.into()),
|
Err(e) => Json(e.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,7 +146,7 @@ pub async fn update_read_access_request(
|
||||||
message: "Community updated".to_string(),
|
message: "Community updated".to_string(),
|
||||||
payload: (),
|
payload: (),
|
||||||
}),
|
}),
|
||||||
Err(e) => return Json(e.into()),
|
Err(e) => Json(e.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,6 +171,176 @@ pub async fn update_write_access_request(
|
||||||
message: "Community updated".to_string(),
|
message: "Community updated".to_string(),
|
||||||
payload: (),
|
payload: (),
|
||||||
}),
|
}),
|
||||||
|
Err(e) => Json(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_membership(
|
||||||
|
jar: CookieJar,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
Path((cid, uid)): Path<(usize, 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 community = match data.get_community_by_id(cid).await {
|
||||||
|
Ok(c) => c,
|
||||||
Err(e) => return Json(e.into()),
|
Err(e) => return Json(e.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if user.id != community.owner {
|
||||||
|
// only the owner can select community memberships
|
||||||
|
return Json(Error::NotAllowed.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
match data.get_membership_by_owner_community(uid, cid).await {
|
||||||
|
Ok(m) => Json(ApiReturn {
|
||||||
|
ok: true,
|
||||||
|
message: "Membership exists".to_string(),
|
||||||
|
payload: Some(m),
|
||||||
|
}),
|
||||||
|
Err(e) => Json(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_membership(
|
||||||
|
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
|
||||||
|
.create_membership(CommunityMembership::new(
|
||||||
|
user.id,
|
||||||
|
id,
|
||||||
|
CommunityPermission::default(),
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => Json(ApiReturn {
|
||||||
|
ok: true,
|
||||||
|
message: "Community joined".to_string(),
|
||||||
|
payload: (),
|
||||||
|
}),
|
||||||
|
Err(e) => Json(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_membership(
|
||||||
|
jar: CookieJar,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
Path((cid, uid)): Path<(usize, 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 membership = match data.get_membership_by_owner_community(uid, cid).await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return Json(e.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
match data.delete_membership(membership.id, user).await {
|
||||||
|
Ok(_) => Json(ApiReturn {
|
||||||
|
ok: true,
|
||||||
|
message: "Membership deleted".to_string(),
|
||||||
|
payload: (),
|
||||||
|
}),
|
||||||
|
Err(e) => Json(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_membership_role(
|
||||||
|
jar: CookieJar,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
Path((cid, uid)): Path<(usize, usize)>,
|
||||||
|
Json(req): Json<UpdateMembershipRole>,
|
||||||
|
) -> 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 membership = match data.get_membership_by_owner_community(uid, cid).await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return Json(e.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let community = match data.get_community_by_id(membership.community).await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return Json(e.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if membership.owner == community.owner {
|
||||||
|
return Json(Error::MiscError("Cannot update community owner's role".to_string()).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.id != community.owner {
|
||||||
|
return Json(Error::NotAllowed.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
match data.update_membership_role(membership.id, req.role).await {
|
||||||
|
Ok(_) => {
|
||||||
|
// check if the user was just banned/unbanned (and send notifs)
|
||||||
|
if (req.role & CommunityPermission::BANNED) == CommunityPermission::BANNED {
|
||||||
|
// user was banned
|
||||||
|
if let Err(e) = data
|
||||||
|
.create_notification(Notification::new(
|
||||||
|
"You have been banned from a community.".to_string(),
|
||||||
|
format!(
|
||||||
|
"You have been banned from [{}](/community/{}).",
|
||||||
|
community.title, community.title
|
||||||
|
),
|
||||||
|
membership.owner,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
return Json(e.into());
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = data.decr_community_member_count(community.id).await {
|
||||||
|
// banned members do not count towards member count
|
||||||
|
return Json(e.into());
|
||||||
|
}
|
||||||
|
} else if (membership.role & CommunityPermission::BANNED) == CommunityPermission::BANNED
|
||||||
|
{
|
||||||
|
// user was unbanned
|
||||||
|
if let Err(e) = data
|
||||||
|
.create_notification(Notification::new(
|
||||||
|
"You have been unbanned from a community.".to_string(),
|
||||||
|
format!(
|
||||||
|
"You have been unbanned from [{}](/community/{}).",
|
||||||
|
community.title, community.title
|
||||||
|
),
|
||||||
|
membership.owner,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
return Json(e.into());
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = data.incr_community_member_count(community.id).await {
|
||||||
|
return Json(e.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Json(ApiReturn {
|
||||||
|
ok: true,
|
||||||
|
message: "Membership updated".to_string(),
|
||||||
|
payload: (),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(e) => Json(e.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,14 +120,11 @@ pub async fn upload_avatar_request(
|
||||||
Err(e) => return Json(e.into()),
|
Err(e) => return Json(e.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if auth_user.id != community.owner {
|
if auth_user.id != community.owner && !auth_user
|
||||||
if !auth_user
|
|
||||||
.permissions
|
.permissions
|
||||||
.check(FinePermission::MANAGE_COMMUNITIES)
|
.check(FinePermission::MANAGE_COMMUNITIES) {
|
||||||
{
|
|
||||||
return Json(Error::NotAllowed.into());
|
return Json(Error::NotAllowed.into());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let path = pathd!(
|
let path = pathd!(
|
||||||
"{}/community_avatars/{}.avif",
|
"{}/community_avatars/{}.avif",
|
||||||
|
@ -176,14 +173,11 @@ pub async fn upload_banner_request(
|
||||||
Err(e) => return Json(e.into()),
|
Err(e) => return Json(e.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if auth_user.id != community.owner {
|
if auth_user.id != community.owner && !auth_user
|
||||||
if !auth_user
|
|
||||||
.permissions
|
.permissions
|
||||||
.check(FinePermission::MANAGE_COMMUNITIES)
|
.check(FinePermission::MANAGE_COMMUNITIES) {
|
||||||
{
|
|
||||||
return Json(Error::NotAllowed.into());
|
return Json(Error::NotAllowed.into());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let path = pathd!(
|
let path = pathd!(
|
||||||
"{}/community_banners/{}.avif",
|
"{}/community_banners/{}.avif",
|
||||||
|
|
|
@ -42,7 +42,7 @@ pub async fn create_request(
|
||||||
message: "Post created".to_string(),
|
message: "Post created".to_string(),
|
||||||
payload: Some(id.to_string()),
|
payload: Some(id.to_string()),
|
||||||
}),
|
}),
|
||||||
Err(e) => return Json(e.into()),
|
Err(e) => Json(e.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ pub async fn delete_request(
|
||||||
message: "Post deleted".to_string(),
|
message: "Post deleted".to_string(),
|
||||||
payload: (),
|
payload: (),
|
||||||
}),
|
}),
|
||||||
Err(e) => return Json(e.into()),
|
Err(e) => Json(e.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,7 +85,7 @@ pub async fn update_content_request(
|
||||||
message: "Post updated".to_string(),
|
message: "Post updated".to_string(),
|
||||||
payload: (),
|
payload: (),
|
||||||
}),
|
}),
|
||||||
Err(e) => return Json(e.into()),
|
Err(e) => Json(e.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,6 +107,6 @@ pub async fn update_context_request(
|
||||||
message: "Post updated".to_string(),
|
message: "Post updated".to_string(),
|
||||||
payload: (),
|
payload: (),
|
||||||
}),
|
}),
|
||||||
Err(e) => return Json(e.into()),
|
Err(e) => Json(e.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ use axum::{
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tetratto_core::model::{
|
use tetratto_core::model::{
|
||||||
communities::{CommunityContext, CommunityReadAccess, CommunityWriteAccess, PostContext},
|
communities::{CommunityContext, CommunityReadAccess, CommunityWriteAccess, PostContext},
|
||||||
|
communities_permissions::CommunityPermission,
|
||||||
reactions::AssetType,
|
reactions::AssetType,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -139,6 +140,23 @@ pub fn routes() -> Router {
|
||||||
"/notifications/{id}/read_status",
|
"/notifications/{id}/read_status",
|
||||||
post(notifications::update_read_status_request),
|
post(notifications::update_read_status_request),
|
||||||
)
|
)
|
||||||
|
// community memberships
|
||||||
|
.route(
|
||||||
|
"/communities/{id}/join",
|
||||||
|
post(communities::communities::create_membership),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/communities/{cid}/memberships/{uid}",
|
||||||
|
get(communities::communities::get_membership),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/communities/{cid}/memberships/{uid}",
|
||||||
|
delete(communities::communities::delete_membership),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/communities/{cid}/memberships/{uid}/role",
|
||||||
|
post(communities::communities::update_membership_role),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -217,3 +235,8 @@ pub struct UpdateUserIsVerified {
|
||||||
pub struct UpdateNotificationRead {
|
pub struct UpdateNotificationRead {
|
||||||
pub read: bool,
|
pub read: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct UpdateMembershipRole {
|
||||||
|
pub role: CommunityPermission,
|
||||||
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ pub async fn delete_request(
|
||||||
message: "Notification deleted".to_string(),
|
message: "Notification deleted".to_string(),
|
||||||
payload: (),
|
payload: (),
|
||||||
}),
|
}),
|
||||||
Err(e) => return Json(e.into()),
|
Err(e) => Json(e.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ pub async fn delete_all_request(
|
||||||
message: "Notifications deleted".to_string(),
|
message: "Notifications deleted".to_string(),
|
||||||
payload: (),
|
payload: (),
|
||||||
}),
|
}),
|
||||||
Err(e) => return Json(e.into()),
|
Err(e) => Json(e.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,6 +65,6 @@ pub async fn update_read_status_request(
|
||||||
message: "Notification updated".to_string(),
|
message: "Notification updated".to_string(),
|
||||||
payload: (),
|
payload: (),
|
||||||
}),
|
}),
|
||||||
Err(e) => return Json(e.into()),
|
Err(e) => Json(e.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ pub async fn get_request(
|
||||||
message: "Reaction exists".to_string(),
|
message: "Reaction exists".to_string(),
|
||||||
payload: Some(r),
|
payload: Some(r),
|
||||||
}),
|
}),
|
||||||
Err(e) => return Json(e.into()),
|
Err(e) => Json(e.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,6 +98,6 @@ pub async fn delete_request(
|
||||||
message: "Reaction deleted".to_string(),
|
message: "Reaction deleted".to_string(),
|
||||||
payload: (),
|
payload: (),
|
||||||
}),
|
}),
|
||||||
Err(e) => return Json(e.into()),
|
Err(e) => Json(e.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,11 +15,9 @@ pub async fn login_request(jar: CookieJar, Extension(data): Extension<State>) ->
|
||||||
}
|
}
|
||||||
|
|
||||||
let lang = get_lang!(jar, data.0);
|
let lang = get_lang!(jar, data.0);
|
||||||
let mut context = initial_context(&data.0.0, lang, &user).await;
|
let context = initial_context(&data.0.0, lang, &user).await;
|
||||||
|
|
||||||
Ok(Html(
|
Ok(Html(data.1.render("auth/login.html", &context).unwrap()))
|
||||||
data.1.render("auth/login.html", &mut context).unwrap(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `/auth/register`
|
/// `/auth/register`
|
||||||
|
@ -35,9 +33,7 @@ pub async fn register_request(
|
||||||
}
|
}
|
||||||
|
|
||||||
let lang = get_lang!(jar, data.0);
|
let lang = get_lang!(jar, data.0);
|
||||||
let mut context = initial_context(&data.0.0, lang, &user).await;
|
let context = initial_context(&data.0.0, lang, &user).await;
|
||||||
|
|
||||||
Ok(Html(
|
Ok(Html(data.1.render("auth/register.html", &context).unwrap()))
|
||||||
data.1.render("auth/register.html", &mut context).unwrap(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,38 @@ macro_rules! check_permissions {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro_rules! community_context_bools {
|
||||||
|
($data:ident, $user:ident, $community:ident) => {{
|
||||||
|
let is_owner = if let Some(ref ua) = $user {
|
||||||
|
ua.id == $community.owner
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
let is_joined = if let Some(ref ua) = $user {
|
||||||
|
if let Ok(membership) = $data
|
||||||
|
.0
|
||||||
|
.get_membership_by_owner_community(ua.id, $community.id)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
membership.role.check_member()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
let can_post = if let Some(ref ua) = $user {
|
||||||
|
$data.0.check_can_post(&$community, ua.id).await
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
(is_owner, is_joined, can_post)
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
/// `/communities`
|
/// `/communities`
|
||||||
pub async fn list_request(jar: CookieJar, Extension(data): Extension<State>) -> impl IntoResponse {
|
pub async fn list_request(jar: CookieJar, Extension(data): Extension<State>) -> impl IntoResponse {
|
||||||
let data = data.read().await;
|
let data = data.read().await;
|
||||||
|
@ -65,9 +97,7 @@ pub async fn list_request(jar: CookieJar, Extension(data): Extension<State>) ->
|
||||||
|
|
||||||
// return
|
// return
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
data.1
|
data.1.render("communities/list.html", &context).unwrap(),
|
||||||
.render("communities/list.html", &mut context)
|
|
||||||
.unwrap(),
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,10 +106,12 @@ pub fn community_context(
|
||||||
community: &Community,
|
community: &Community,
|
||||||
is_owner: bool,
|
is_owner: bool,
|
||||||
is_joined: bool,
|
is_joined: bool,
|
||||||
|
can_post: bool,
|
||||||
) {
|
) {
|
||||||
context.insert("community", &community);
|
context.insert("community", &community);
|
||||||
context.insert("is_owner", &is_owner);
|
context.insert("is_owner", &is_owner);
|
||||||
context.insert("is_joined", &is_joined);
|
context.insert("is_joined", &is_joined);
|
||||||
|
context.insert("can_post", &can_post);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `/community/{title}`
|
/// `/community/{title}`
|
||||||
|
@ -117,29 +149,14 @@ pub async fn feed_request(
|
||||||
let lang = get_lang!(jar, data.0);
|
let lang = get_lang!(jar, data.0);
|
||||||
let mut context = initial_context(&data.0.0, lang, &user).await;
|
let mut context = initial_context(&data.0.0, lang, &user).await;
|
||||||
|
|
||||||
let is_owner = if let Some(ref ua) = user {
|
let (is_owner, is_joined, can_post) = community_context_bools!(data, user, community);
|
||||||
ua.id == community.owner
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
let is_joined = if let Some(ref ua) = user {
|
|
||||||
data.0
|
|
||||||
.get_membership_by_owner_community(ua.id, community.id)
|
|
||||||
.await
|
|
||||||
.is_ok()
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
context.insert("feed", &feed);
|
context.insert("feed", &feed);
|
||||||
community_context(&mut context, &community, is_owner, is_joined);
|
community_context(&mut context, &community, is_owner, is_joined, can_post);
|
||||||
|
|
||||||
// return
|
// return
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
data.1
|
data.1.render("communities/feed.html", &context).unwrap(),
|
||||||
.render("communities/feed.html", &mut context)
|
|
||||||
.unwrap(),
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,7 +202,7 @@ pub async fn settings_request(
|
||||||
// return
|
// return
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
data.1
|
data.1
|
||||||
.render("communities/settings.html", &mut context)
|
.render("communities/settings.html", &context)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@ -226,20 +243,7 @@ pub async fn post_request(
|
||||||
let lang = get_lang!(jar, data.0);
|
let lang = get_lang!(jar, data.0);
|
||||||
let mut context = initial_context(&data.0.0, lang, &user).await;
|
let mut context = initial_context(&data.0.0, lang, &user).await;
|
||||||
|
|
||||||
let is_owner = if let Some(ref ua) = user {
|
let (is_owner, is_joined, can_post) = community_context_bools!(data, user, community);
|
||||||
ua.id == community.owner
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
let is_joined = if let Some(ref ua) = user {
|
|
||||||
data.0
|
|
||||||
.get_membership_by_owner_community(ua.id, community.id)
|
|
||||||
.await
|
|
||||||
.is_ok()
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
context.insert("post", &post);
|
context.insert("post", &post);
|
||||||
context.insert("replies", &feed);
|
context.insert("replies", &feed);
|
||||||
|
@ -251,12 +255,10 @@ pub async fn post_request(
|
||||||
.await
|
.await
|
||||||
.unwrap_or(User::deleted()),
|
.unwrap_or(User::deleted()),
|
||||||
);
|
);
|
||||||
community_context(&mut context, &community, is_owner, is_joined);
|
community_context(&mut context, &community, is_owner, is_joined, can_post);
|
||||||
|
|
||||||
// return
|
// return
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
data.1
|
data.1.render("communities/post.html", &context).unwrap(),
|
||||||
.render("communities/post.html", &mut context)
|
|
||||||
.unwrap(),
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,9 +27,9 @@ pub async fn index_request(jar: CookieJar, Extension(data): Extension<State>) ->
|
||||||
let user = get_user_from_token!(jar, data.0);
|
let user = get_user_from_token!(jar, data.0);
|
||||||
|
|
||||||
let lang = get_lang!(jar, data.0);
|
let lang = get_lang!(jar, data.0);
|
||||||
let mut context = initial_context(&data.0.0, lang, &user).await;
|
let context = initial_context(&data.0.0, lang, &user).await;
|
||||||
|
|
||||||
Html(data.1.render("misc/index.html", &mut context).unwrap())
|
Html(data.1.render("misc/index.html", &context).unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `/notifs`
|
/// `/notifs`
|
||||||
|
@ -58,8 +58,6 @@ pub async fn notifications_request(
|
||||||
|
|
||||||
// return
|
// return
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
data.1
|
data.1.render("misc/notifications.html", &context).unwrap(),
|
||||||
.render("misc/notifications.html", &mut context)
|
|
||||||
.unwrap(),
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,9 +42,9 @@ pub async fn render_error(
|
||||||
user: &Option<User>,
|
user: &Option<User>,
|
||||||
) -> String {
|
) -> String {
|
||||||
let lang = get_lang!(jar, data.0);
|
let lang = get_lang!(jar, data.0);
|
||||||
let mut context = initial_context(&data.0.0, lang, &user).await;
|
let mut context = initial_context(&data.0.0, lang, user).await;
|
||||||
context.insert("error_text", &e.to_string());
|
context.insert("error_text", &e.to_string());
|
||||||
data.1.render("misc/error.html", &mut context).unwrap()
|
data.1.render("misc/error.html", &context).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|
|
@ -45,9 +45,7 @@ pub async fn settings_request(
|
||||||
|
|
||||||
// return
|
// return
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
data.1
|
data.1.render("profile/settings.html", &context).unwrap(),
|
||||||
.render("profile/settings.html", &mut context)
|
|
||||||
.unwrap(),
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,7 +141,5 @@ pub async fn posts_request(
|
||||||
);
|
);
|
||||||
|
|
||||||
// return
|
// return
|
||||||
Ok(Html(
|
Ok(Html(data.1.render("profile/posts.html", &context).unwrap()))
|
||||||
data.1.render("profile/posts.html", &mut context).unwrap(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ impl DataManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto_method!(get_membership_by_id()@get_membership_from_row -> "SELECT * FROM memberships WHERE id = $1" --name="journal membership" --returns=CommunityMembership --cache-key-tmpl="atto.membership:{}");
|
auto_method!(get_membership_by_id()@get_membership_from_row -> "SELECT * FROM memberships WHERE id = $1" --name="community membership" --returns=CommunityMembership --cache-key-tmpl="atto.membership:{}");
|
||||||
|
|
||||||
/// Replace a list of community memberships with the proper community.
|
/// Replace a list of community memberships with the proper community.
|
||||||
pub async fn fill_communities(&self, list: Vec<CommunityMembership>) -> Result<Vec<Community>> {
|
pub async fn fill_communities(&self, list: Vec<CommunityMembership>) -> Result<Vec<Community>> {
|
||||||
|
@ -73,7 +73,7 @@ impl DataManager {
|
||||||
|
|
||||||
let res = query_rows!(
|
let res = query_rows!(
|
||||||
&conn,
|
&conn,
|
||||||
"SELECT * FROM memberships WHERE owner = $1",
|
"SELECT * FROM memberships WHERE owner = $1 AND role IS NOT 33",
|
||||||
&[&(owner as i64)],
|
&[&(owner as i64)],
|
||||||
|x| { Self::get_membership_from_row(x) }
|
|x| { Self::get_membership_from_row(x) }
|
||||||
);
|
);
|
||||||
|
@ -90,6 +90,16 @@ impl DataManager {
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// * `data` - a mock [`CommunityMembership`] object to insert
|
/// * `data` - a mock [`CommunityMembership`] object to insert
|
||||||
pub async fn create_membership(&self, data: CommunityMembership) -> Result<()> {
|
pub async fn create_membership(&self, data: CommunityMembership) -> Result<()> {
|
||||||
|
// make sure membership doesn't already exist
|
||||||
|
if self
|
||||||
|
.get_membership_by_owner_community(data.owner, data.community)
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
return Err(Error::MiscError("Already joined community".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
let conn = match self.connect().await {
|
let conn = match self.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())),
|
||||||
|
|
|
@ -167,6 +167,35 @@ impl DataManager {
|
||||||
Ok(res.unwrap())
|
Ok(res.unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if the given `uid` can post in the given `community`.
|
||||||
|
pub async fn check_can_post(&self, community: &Community, uid: usize) -> bool {
|
||||||
|
match community.write_access {
|
||||||
|
CommunityWriteAccess::Owner => {
|
||||||
|
if uid != community.owner {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CommunityWriteAccess::Joined => {
|
||||||
|
match self
|
||||||
|
.get_membership_by_owner_community(uid, community.id)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(m) => {
|
||||||
|
if !m.role.check_member() {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a new journal entry in the database.
|
/// Create a new journal entry in the database.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
|
@ -185,22 +214,9 @@ impl DataManager {
|
||||||
Err(e) => return Err(e),
|
Err(e) => return Err(e),
|
||||||
};
|
};
|
||||||
|
|
||||||
match community.write_access {
|
if !self.check_can_post(&community, data.owner).await {
|
||||||
CommunityWriteAccess::Owner => {
|
|
||||||
if data.owner != community.owner {
|
|
||||||
return Err(Error::NotAllowed);
|
return Err(Error::NotAllowed);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
CommunityWriteAccess::Joined => {
|
|
||||||
if let Err(_) = self
|
|
||||||
.get_membership_by_owner_community(data.owner, community.id)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
return Err(Error::NotAllowed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
};
|
|
||||||
|
|
||||||
// check if we're blocked
|
// check if we're blocked
|
||||||
if let Some(replying_to) = data.replying_to {
|
if let Some(replying_to) = data.replying_to {
|
||||||
|
|
|
@ -57,7 +57,7 @@ impl DataManager {
|
||||||
Ok(res.unwrap())
|
Ok(res.unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new journal membership in the database.
|
/// Create a new reaction in the database.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// * `data` - a mock [`Reaction`] object to insert
|
/// * `data` - a mock [`Reaction`] object to insert
|
||||||
|
|
|
@ -13,6 +13,7 @@ bitflags! {
|
||||||
const MEMBER = 1 << 2;
|
const MEMBER = 1 << 2;
|
||||||
const MANAGE_POSTS = 1 << 3;
|
const MANAGE_POSTS = 1 << 3;
|
||||||
const MANAGE_ROLES = 1 << 4;
|
const MANAGE_ROLES = 1 << 4;
|
||||||
|
const BANNED = 1 << 5;
|
||||||
|
|
||||||
const _ = !0;
|
const _ = !0;
|
||||||
}
|
}
|
||||||
|
@ -89,6 +90,9 @@ impl CommunityPermission {
|
||||||
if (self & CommunityPermission::ADMINISTRATOR) == CommunityPermission::ADMINISTRATOR {
|
if (self & CommunityPermission::ADMINISTRATOR) == CommunityPermission::ADMINISTRATOR {
|
||||||
// has administrator permission, meaning everything else is automatically true
|
// has administrator permission, meaning everything else is automatically true
|
||||||
return true;
|
return true;
|
||||||
|
} else if (self & CommunityPermission::BANNED) == CommunityPermission::BANNED {
|
||||||
|
// has banned permission, meaning everything else is automatically false
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
(self & permission) == permission
|
(self & permission) == permission
|
||||||
|
@ -107,6 +111,6 @@ impl CommunityPermission {
|
||||||
|
|
||||||
impl Default for CommunityPermission {
|
impl Default for CommunityPermission {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::DEFAULT
|
Self::DEFAULT | Self::MEMBER
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue