add: ability to ip block users from their profile
This commit is contained in:
parent
a799c777ea
commit
0163391380
12 changed files with 241 additions and 20 deletions
|
@ -169,6 +169,7 @@ version = "1.0.0"
|
|||
"settings:label.export" = "Export"
|
||||
"settings:label.manage_blocks" = "Manage blocks"
|
||||
"settings:label.users" = "Users"
|
||||
"settings:label.ips" = "IPs"
|
||||
"settings:label.generate_invites" = "Generate invites"
|
||||
"settings:label.add_to_stack" = "Add to stack"
|
||||
"settings:tab.security" = "Security"
|
||||
|
|
|
@ -38,6 +38,10 @@
|
|||
--pad-2: 0.5rem;
|
||||
--pad-3: 0.75rem;
|
||||
--pad-4: 1rem;
|
||||
|
||||
--online: var(--color-green);
|
||||
--idle: var(--color-yellow);
|
||||
--offline: hsl(0, 0%, 50%);
|
||||
}
|
||||
|
||||
.dark,
|
||||
|
|
|
@ -528,7 +528,7 @@
|
|||
("width" "24")
|
||||
("height" "24")
|
||||
("viewBox" "0 0 24 24")
|
||||
("style" "fill: var(--color-green)")
|
||||
("style" "fill: var(--online)")
|
||||
(circle
|
||||
("cx" "12")
|
||||
("cy" "12")
|
||||
|
@ -541,7 +541,7 @@
|
|||
("width" "24")
|
||||
("height" "24")
|
||||
("viewBox" "0 0 24 24")
|
||||
("style" "fill: var(--color-yellow)")
|
||||
("style" "fill: var(--idle)")
|
||||
(circle
|
||||
("cx" "12")
|
||||
("cy" "12")
|
||||
|
@ -554,7 +554,7 @@
|
|||
("width" "24")
|
||||
("height" "24")
|
||||
("viewBox" "0 0 24 24")
|
||||
("style" "fill: hsl(0, 0%, 50%)")
|
||||
("style" "fill: var(--offline)")
|
||||
(circle
|
||||
("cx" "12")
|
||||
("cy" "12")
|
||||
|
@ -611,7 +611,8 @@
|
|||
(text "{%- endif %}")
|
||||
(div
|
||||
("style" "display: none;")
|
||||
(text "{{ self::theme_color(color=user.settings.theme_color_surface, css=\"color-surface\") }} {{ self::theme_color(color=user.settings.theme_color_text, css=\"color-text\") }} {{ self::theme_color(color=user.settings.theme_color_text_link, css=\"color-link\") }} {{ self::theme_color(color=user.settings.theme_color_lowered, css=\"color-lowered\") }} {{ self::theme_color(color=user.settings.theme_color_text_lowered, css=\"color-text-lowered\") }} {{ self::theme_color(color=user.settings.theme_color_super_lowered, css=\"color-super-lowered\") }} {{ self::theme_color(color=user.settings.theme_color_raised, css=\"color-raised\") }} {{ self::theme_color(color=user.settings.theme_color_text_raised, css=\"color-text-raised\") }} {{ self::theme_color(color=user.settings.theme_color_super_raised, css=\"color-super-raised\") }} {{ self::theme_color(color=user.settings.theme_color_primary, css=\"color-primary\") }} {{ self::theme_color(color=user.settings.theme_color_text_primary, css=\"color-text-primary\") }} {{ self::theme_color(color=user.settings.theme_color_primary_lowered, css=\"color-primary-lowered\") }} {{ self::theme_color(color=user.settings.theme_color_secondary, css=\"color-secondary\") }} {{ self::theme_color(color=user.settings.theme_color_text_secondary, css=\"color-text-secondary\") }} {{ self::theme_color(color=user.settings.theme_color_secondary_lowered, css=\"color-secondary-lowered\") }} {% if user.permissions|has_supporter -%}")
|
||||
(text "{{ self::theme_color(color=user.settings.theme_color_surface, css=\"color-surface\") }} {{ self::theme_color(color=user.settings.theme_color_text, css=\"color-text\") }} {{ self::theme_color(color=user.settings.theme_color_text_link, css=\"color-link\") }} {{ self::theme_color(color=user.settings.theme_color_lowered, css=\"color-lowered\") }} {{ self::theme_color(color=user.settings.theme_color_text_lowered, css=\"color-text-lowered\") }} {{ self::theme_color(color=user.settings.theme_color_super_lowered, css=\"color-super-lowered\") }} {{ self::theme_color(color=user.settings.theme_color_raised, css=\"color-raised\") }} {{ self::theme_color(color=user.settings.theme_color_text_raised, css=\"color-text-raised\") }} {{ self::theme_color(color=user.settings.theme_color_super_raised, css=\"color-super-raised\") }} {{ self::theme_color(color=user.settings.theme_color_primary, css=\"color-primary\") }} {{ self::theme_color(color=user.settings.theme_color_text_primary, css=\"color-text-primary\") }} {{ self::theme_color(color=user.settings.theme_color_primary_lowered, css=\"color-primary-lowered\") }} {{ self::theme_color(color=user.settings.theme_color_secondary, css=\"color-secondary\") }} {{ self::theme_color(color=user.settings.theme_color_text_secondary, css=\"color-text-secondary\") }} {{ self::theme_color(color=user.settings.theme_color_secondary_lowered, css=\"color-secondary-lowered\") }}
|
||||
{{ self::theme_color(color=user.settings.theme_color_online, css=\"online\") }} {{ self::theme_color(color=user.settings.theme_color_idle, css=\"idle\") }} {{ self::theme_color(color=user.settings.theme_color_offline, css=\"offline\") }} {% if user.permissions|has_supporter -%}")
|
||||
(style
|
||||
(text "{{ user.settings.theme_custom_css|remove_script_tags|safe }}"))
|
||||
(text "{%- endif %}"))
|
||||
|
|
|
@ -219,12 +219,24 @@
|
|||
(text "{{ icon \"user-minus\" }}")
|
||||
(span
|
||||
(text "{{ text \"auth:action.unfollow\" }}")))
|
||||
(button
|
||||
("onclick" "toggle_block_user()")
|
||||
("class" "lowered red")
|
||||
(text "{{ icon \"shield\" }}")
|
||||
(span
|
||||
(text "{{ text \"auth:action.block\" }}")))
|
||||
(div
|
||||
("class" "dropdown")
|
||||
(button
|
||||
("onclick" "trigger('atto::hooks::dropdown', [event])")
|
||||
("exclude" "dropdown")
|
||||
("class" "lowered red")
|
||||
(icon_class (text "chevron-down") (text "dropdown-arrow"))
|
||||
(str (text "auth:action.block")))
|
||||
(div
|
||||
("class" "inner")
|
||||
(button
|
||||
("onclick" "toggle_block_user()")
|
||||
(icon (text "shield"))
|
||||
(str (text "auth:action.block")))
|
||||
(button
|
||||
("onclick" "ip_block_user()")
|
||||
(icon (text "wifi"))
|
||||
(str (text "auth:action.ip_block")))))
|
||||
(text "{% else %}")
|
||||
(button
|
||||
("onclick" "toggle_block_user()")
|
||||
|
@ -342,6 +354,30 @@
|
|||
res.message,
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
globalThis.ip_block_user = async () => {
|
||||
if (
|
||||
!(await trigger(\"atto::confirm\", [
|
||||
\"Are you sure you would like to do this?\",
|
||||
]))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(
|
||||
\"/api/v1/auth/user/{{ profile.id }}/block_ip\",
|
||||
{
|
||||
method: \"POST\",
|
||||
},
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
trigger(\"atto::toast\", [
|
||||
res.ok ? \"success\" : \"error\",
|
||||
res.message,
|
||||
]);
|
||||
});
|
||||
};"))))
|
||||
(text "{%- endif %} {% if not profile.settings.private_communities or is_self or is_helper %}")
|
||||
(div
|
||||
|
|
|
@ -446,6 +446,30 @@
|
|||
("class" "button lowered small")
|
||||
(icon (text "external-link"))
|
||||
(span (str (text "requests:action.view_profile"))))))
|
||||
(text "{% endfor %}")))
|
||||
|
||||
; ip blocks
|
||||
(div
|
||||
("class" "card-nest")
|
||||
(div
|
||||
("class" "card flex items-center gap-2 small")
|
||||
(text "{{ icon \"wifi\" }}")
|
||||
(span
|
||||
(text "{{ text \"settings:label.ips\" }}")))
|
||||
(div
|
||||
("class" "card flex flex-col gap-2")
|
||||
(text "{% for ip in ipblocks %}")
|
||||
(div
|
||||
("class" "card secondary flex flex-wrap gap-2 items-center justify-between")
|
||||
(span
|
||||
(text "Block from: ") (span ("class" "date") (text "{{ ip.created }}")))
|
||||
(div
|
||||
("class" "flex gap-2")
|
||||
(button
|
||||
("onclick" "trigger('me::remove_ip_block', ['{{ ip.id }}'])")
|
||||
("class" "lowered small red")
|
||||
(icon (text "x"))
|
||||
(span (str (text "auth:action.unblock"))))))
|
||||
(text "{% endfor %}")))))
|
||||
(div
|
||||
("class" "w-full flex flex-col gap-2 hidden")
|
||||
|
@ -1734,6 +1758,35 @@
|
|||
description: \"Hover state for secondary buttons.\",
|
||||
},
|
||||
],
|
||||
// online indicator
|
||||
[[], \"\", \"divider\"],
|
||||
[
|
||||
[\"theme_color_online\", \"Online indicator (online)\"],
|
||||
\"{{ profile.settings.theme_color_online }}\",
|
||||
\"color\",
|
||||
{
|
||||
description:
|
||||
\"The green dot next to the name of online users.\",
|
||||
},
|
||||
],
|
||||
[
|
||||
[\"theme_color_idle\", \"Online indicator (idle)\"],
|
||||
\"{{ profile.settings.theme_color_idle }}\",
|
||||
\"color\",
|
||||
{
|
||||
description:
|
||||
\"The yellow dot next to the name of online users.\",
|
||||
},
|
||||
],
|
||||
[
|
||||
[\"theme_color_offline\", \"Online indicator (offline)\"],
|
||||
\"{{ profile.settings.theme_color_offline }}\",
|
||||
\"color\",
|
||||
{
|
||||
description:
|
||||
\"The grey next to the name of online users.\",
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
if (can_use_custom_css) {
|
||||
|
|
|
@ -505,7 +505,7 @@ media_theme_pref();
|
|||
return now - last_seen <= maximum_time_to_be_considered_idle;
|
||||
});
|
||||
|
||||
self.define("hooks::online_indicator", ({ $ }) => {
|
||||
self.define("hooks::online_indicator", async ({ $ }) => {
|
||||
for (const element of Array.from(
|
||||
document.querySelectorAll("[hook=online_indicator]") || [],
|
||||
)) {
|
||||
|
@ -513,8 +513,8 @@ media_theme_pref();
|
|||
element.getAttribute("hook-arg:last_seen"),
|
||||
);
|
||||
|
||||
const is_online = $.last_seen_just_now(last_seen);
|
||||
const is_idle = $.last_seen_recently(last_seen);
|
||||
const is_online = await $.last_seen_just_now(last_seen);
|
||||
const is_idle = await $.last_seen_recently(last_seen);
|
||||
|
||||
const offline = element.querySelector("[hook_ui_ident=offline]");
|
||||
const online = element.querySelector("[hook_ui_ident=online]");
|
||||
|
|
|
@ -402,6 +402,27 @@
|
|||
});
|
||||
});
|
||||
|
||||
self.define("remove_ip_block", async (_, id) => {
|
||||
if (
|
||||
!(await trigger("atto::confirm", [
|
||||
"Are you sure you want to do this?",
|
||||
]))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/api/v1/auth/ip/${id}/unblock_ip`, {
|
||||
method: "POST",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
trigger("atto::toast", [
|
||||
res.ok ? "success" : "error",
|
||||
res.message,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
self.define("notifications_stream", ({ _, streams }) => {
|
||||
const element = document.getElementById("notifications_span");
|
||||
|
||||
|
|
|
@ -314,3 +314,64 @@ pub async fn following_request(
|
|||
Err(e) => Json(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn ip_block_profile_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, oauth::AppScope::UserCreateIpBlock) {
|
||||
Some(ua) => ua,
|
||||
None => return Json(Error::NotAllowed.into()),
|
||||
};
|
||||
|
||||
// get other user
|
||||
let other_user = match data.get_user_by_id(id).await {
|
||||
Ok(x) => x,
|
||||
Err(e) => return Json(e.into()),
|
||||
};
|
||||
|
||||
for (ip, _, _) in other_user.tokens {
|
||||
// check for an existing ip block
|
||||
if data
|
||||
.get_ipblock_by_initiator_receiver(user.id, &ip)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// create ip block
|
||||
if let Err(e) = data.create_ipblock(IpBlock::new(user.id, ip)).await {
|
||||
return Json(e.into());
|
||||
}
|
||||
}
|
||||
|
||||
Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "IP(s) blocked".to_string(),
|
||||
payload: (),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn remove_ip_block_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, oauth::AppScope::UserManageBlocks) {
|
||||
Some(ua) => ua,
|
||||
None => return Json(Error::NotAllowed.into()),
|
||||
};
|
||||
|
||||
match data.delete_ipblock(id, user).await {
|
||||
Ok(_) => Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "IP unblocked".to_string(),
|
||||
payload: (),
|
||||
}),
|
||||
Err(e) => Json(e.into()),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -288,6 +288,14 @@ pub fn routes() -> Router {
|
|||
post(auth::social::accept_follow_request),
|
||||
)
|
||||
.route("/auth/user/{id}/block", post(auth::social::block_request))
|
||||
.route(
|
||||
"/auth/user/{id}/block_ip",
|
||||
post(auth::social::ip_block_profile_request),
|
||||
)
|
||||
.route(
|
||||
"/auth/ip/{id}/unblock_ip",
|
||||
post(auth::social::remove_ip_block_request),
|
||||
)
|
||||
.route(
|
||||
"/auth/user/{id}/settings",
|
||||
post(auth::profile::update_user_settings_request),
|
||||
|
|
|
@ -94,6 +94,11 @@ pub async fn settings_request(
|
|||
out
|
||||
};
|
||||
|
||||
let ipblocks = match data.0.get_ipblocks_by_initiator(profile.id).await {
|
||||
Ok(l) => l,
|
||||
Err(e) => return Err(Html(render_error(e, &jar, &data, &None).await)),
|
||||
};
|
||||
|
||||
let uploads = match data.0.get_uploads_by_owner(profile.id, 12, req.page).await {
|
||||
Ok(ua) => ua,
|
||||
Err(e) => {
|
||||
|
@ -129,6 +134,7 @@ pub async fn settings_request(
|
|||
context.insert("following", &following);
|
||||
context.insert("blocks", &blocks);
|
||||
context.insert("stackblocks", &stackblocks);
|
||||
context.insert("ipblocks", &ipblocks);
|
||||
context.insert("invites", &invites);
|
||||
context.insert(
|
||||
"user_tokens_serde",
|
||||
|
|
|
@ -2,7 +2,7 @@ use oiseau::cache::Cache;
|
|||
use crate::model::{Error, Result, auth::User, auth::IpBlock, permissions::FinePermission};
|
||||
use crate::{auto_method, DataManager};
|
||||
|
||||
use oiseau::PostgresRow;
|
||||
use oiseau::{query_rows, PostgresRow};
|
||||
|
||||
use oiseau::{execute, get, query_row, params};
|
||||
|
||||
|
@ -19,7 +19,7 @@ impl DataManager {
|
|||
|
||||
auto_method!(get_ipblock_by_id()@get_ipblock_from_row -> "SELECT * FROM ipblocks WHERE id = $1" --name="ip block" --returns=IpBlock --cache-key-tmpl="atto.ipblock:{}");
|
||||
|
||||
/// Get a user block by `initiator` and `receiver` (in that order).
|
||||
/// Get a ip block by `initiator` and `receiver` (in that order).
|
||||
pub async fn get_ipblock_by_initiator_receiver(
|
||||
&self,
|
||||
initiator: usize,
|
||||
|
@ -38,13 +38,13 @@ impl DataManager {
|
|||
);
|
||||
|
||||
if res.is_err() {
|
||||
return Err(Error::GeneralNotFound("user block".to_string()));
|
||||
return Err(Error::GeneralNotFound("ip block".to_string()));
|
||||
}
|
||||
|
||||
Ok(res.unwrap())
|
||||
}
|
||||
|
||||
/// Get a user block by `receiver` and `initiator` (in that order).
|
||||
/// Get a ip block by `receiver` and `initiator` (in that order).
|
||||
pub async fn get_ipblock_by_receiver_initiator(
|
||||
&self,
|
||||
receiver: &str,
|
||||
|
@ -63,13 +63,34 @@ impl DataManager {
|
|||
);
|
||||
|
||||
if res.is_err() {
|
||||
return Err(Error::GeneralNotFound("user block".to_string()));
|
||||
return Err(Error::GeneralNotFound("ip block".to_string()));
|
||||
}
|
||||
|
||||
Ok(res.unwrap())
|
||||
}
|
||||
|
||||
/// Create a new user block in the database.
|
||||
/// Get all ip blocks by `initiator`.
|
||||
pub async fn get_ipblocks_by_initiator(&self, initiator: usize) -> Result<Vec<IpBlock>> {
|
||||
let conn = match self.0.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = query_rows!(
|
||||
&conn,
|
||||
"SELECT * FROM ipblocks WHERE initiator = $1",
|
||||
params![&(initiator as i64)],
|
||||
|x| { Self::get_ipblock_from_row(x) }
|
||||
);
|
||||
|
||||
if res.is_err() {
|
||||
return Err(Error::GeneralNotFound("ip block".to_string()));
|
||||
}
|
||||
|
||||
Ok(res.unwrap())
|
||||
}
|
||||
|
||||
/// Create a new ip block in the database.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `data` - a mock [`IpBlock`] object to insert
|
||||
|
@ -102,7 +123,7 @@ impl DataManager {
|
|||
let block = self.get_ipblock_by_id(id).await?;
|
||||
|
||||
if user.id != block.initiator {
|
||||
// only the initiator (or moderators) can delete user blocks!
|
||||
// only the initiator (or moderators) can delete ip blocks!
|
||||
if !user.permissions.check(FinePermission::MANAGE_FOLLOWS) {
|
||||
return Err(Error::NotAllowed);
|
||||
}
|
||||
|
|
|
@ -192,6 +192,15 @@ pub struct UserSettings {
|
|||
/// Custom CSS input.
|
||||
#[serde(default)]
|
||||
pub theme_custom_css: String,
|
||||
/// The color of an online online indicator.
|
||||
#[serde(default)]
|
||||
pub theme_color_online: String,
|
||||
/// The color of an idle online indicator.
|
||||
#[serde(default)]
|
||||
pub theme_color_idle: String,
|
||||
/// The color of an offline online indicator.
|
||||
#[serde(default)]
|
||||
pub theme_color_offline: String,
|
||||
#[serde(default)]
|
||||
pub disable_other_themes: bool,
|
||||
#[serde(default)]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue