add: anonymous questions

This commit is contained in:
trisua 2025-04-19 18:59:55 -04:00
parent 2266afde01
commit 3db7f2699c
34 changed files with 473 additions and 98 deletions

View file

@ -1,6 +1,6 @@
[package]
name = "tetratto"
version = "1.0.5"
version = "1.0.6"
edition = "2024"
[features]

View file

@ -41,10 +41,11 @@ version = "1.0.0"
"auth:action.login" = "Login"
"auth:action.register" = "Register"
"auth:action.logout" = "Logout"
"auto:action.follow" = "Follow"
"auto:action.unfollow" = "Unfollow"
"auto:action.block" = "Block"
"auto:action.unblock" = "Unblock"
"auth:action.follow" = "Follow"
"auth:action.unfollow" = "Unfollow"
"auth:action.block" = "Block"
"auth:action.ip_block" = "IP block"
"auth:action.unblock" = "Unblock"
"auth:link.my_profile" = "My profile"
"auth:link.settings" = "Settings"
"auth:label.followers" = "Followers"
@ -55,8 +56,8 @@ version = "1.0.0"
"auth:label.before_you_view" = "Before you view"
"auth:label.private_profile" = "Private profile"
"auth:label.private_profile_message" = "This profile is private, meaning you can only view it if they follow you."
"auto:action.request_to_follow" = "Request to follow"
"auto:action.cancel_follow_request" = "Cancel follow request"
"auth:action.request_to_follow" = "Request to follow"
"auth:action.cancel_follow_request" = "Cancel follow request"
"communities:action.create" = "Create"
"communities:action.select" = "Select"

View file

@ -110,7 +110,8 @@
<div class="flex gap-2">
<button class="primary">{{ text "requests:label.answer" }}</button>
<button class="red quaternary" onclick="trigger('me::remove_question', ['{{ question[0].id }}'])">{{ text "general:action.delete" }}</button>
<button type="button" class="red quaternary" onclick="trigger('me::remove_question', ['{{ question[0].id }}'])">{{ text "general:action.delete" }}</button>
<button type="button" class="red quaternary" onclick="trigger('me::ip_block_question', ['{{ question[0].id }}'])">{{ text "auth:action.ip_block" }}</button>
</div>
</form>
</div>

View file

@ -156,7 +156,7 @@
atto_tag="user.follow"
>
{{ icon "user-plus" }}
<span>{{ text "auto:action.follow" }}</span>
<span>{{ text "auth:action.follow" }}</span>
</button>
<button
@ -165,7 +165,7 @@
atto_tag="user.unfollow"
>
{{ icon "user-minus" }}
<span>{{ text "auto:action.unfollow" }}</span>
<span>{{ text "auth:action.unfollow" }}</span>
</button>
<button
@ -173,7 +173,7 @@
class="quaternary red"
>
{{ icon "shield" }}
<span>{{ text "auto:action.block" }}</span>
<span>{{ text "auth:action.block" }}</span>
</button>
{% else %}
<button
@ -181,7 +181,7 @@
class="quaternary red"
>
{{ icon "shield-off" }}
<span>{{ text "auto:action.unblock" }}</span>
<span>{{ text "auth:action.unblock" }}</span>
</button>
{% endif %} {% if is_helper %}
<a

View file

@ -1,5 +1,6 @@
{% extends "profile/base.html" %} {% block content %} {% if
profile.settings.enable_questions and user %}
profile.settings.enable_questions and (user or
profile.settings.allow_anonymous_questions) %}
<div style="display: contents">
{{ components::create_question_form(receiver=profile.id,
header=profile.settings.motivational_header) }}

View file

@ -25,7 +25,7 @@
atto_tag="user.follow_request"
>
{{ icon "user-plus" }}
<span>{{ text "auto:action.request_to_follow" }}</span>
<span>{{ text "auth:action.request_to_follow" }}</span>
</button>
<button
@ -34,7 +34,7 @@
atto_tag="user.cancel_request"
>
{{ icon "user-minus" }}
<span>{{ text "auto:action.cancel_follow_request" }}</span>
<span>{{ text "auth:action.cancel_follow_request" }}</span>
</button>
{% else %}
<button
@ -43,7 +43,7 @@
atto_tag="user.unfollow"
>
{{ icon "user-minus" }}
<span>{{ text "auto:action.unfollow" }}</span>
<span>{{ text "auth:action.unfollow" }}</span>
</button>
{% endif %}

View file

@ -748,20 +748,6 @@
settings.warning,
"textarea",
],
[[], "Questions", "title"],
[
[
"enable_questions",
"Allow users to ask you questions",
],
"{{ profile.settings.enable_questions }}",
"checkbox",
],
[
["motivational_header", "Motivational header"],
settings.motivational_header,
"input",
],
],
settings,
);
@ -791,6 +777,28 @@
"{{ profile.settings.private_last_seen }}",
"checkbox",
],
[[], "Questions", "title"],
[
[
"enable_questions",
"Allow users to ask you questions",
],
"{{ profile.settings.enable_questions }}",
"checkbox",
],
[
[
"allow_anonymous_questions",
"Allow anonymous questions",
],
"{{ profile.settings.allow_anonymous_questions }}",
"checkbox",
],
[
["motivational_header", "Motivational header"],
settings.motivational_header,
"input",
],
],
settings,
);

View file

@ -214,6 +214,27 @@
});
});
self.define("ip_block_question", async (_, id) => {
if (
!(await trigger("atto::confirm", [
"Are you sure you want to do this?",
]))
) {
return;
}
fetch(`/api/v1/questions/${id}/block_ip`, {
method: "POST",
})
.then((res) => res.json())
.then((res) => {
trigger("atto::toast", [
res.ok ? "success" : "error",
res.message,
]);
});
});
// token switcher
self.define(
"set_login_account_tokens",

View file

@ -4,7 +4,7 @@ use crate::{
};
use axum::{Extension, Json, extract::Path, response::IntoResponse};
use axum_extra::extract::CookieJar;
use tetratto_core::model::auth::{FollowResult, Notification, UserBlock, UserFollow};
use tetratto_core::model::auth::{FollowResult, IpBlock, Notification, UserBlock, UserFollow};
/// Toggle following on the given user.
pub async fn follow_request(
@ -197,3 +197,38 @@ pub async fn block_request(
}
}
}
/// Toggle IP blocking on the given IP.
pub async fn ip_block_request(
jar: CookieJar,
Path(ip): Path<String>,
Extension(data): Extension<State>,
) -> 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()),
};
if let Ok(ipblock) = data.get_ipblock_by_initiator_receiver(user.id, &ip).await {
// delete
match data.delete_ipblock(ipblock.id, user).await {
Ok(_) => Json(ApiReturn {
ok: true,
message: "IP unblocked".to_string(),
payload: (),
}),
Err(e) => Json(e.into()),
}
} else {
// create
match data.create_ipblock(IpBlock::new(user.id, ip)).await {
Ok(_) => Json(ApiReturn {
ok: true,
message: "IP blocked".to_string(),
payload: (),
}),
Err(e) => Json(e.into()),
}
}
}

View file

@ -1,27 +1,49 @@
use axum::{Extension, Json, extract::Path, response::IntoResponse};
use axum::{
extract::Path,
http::{HeaderMap, HeaderValue},
response::IntoResponse,
Extension, Json,
};
use axum_extra::extract::CookieJar;
use tetratto_core::model::{communities::Question, ApiReturn, Error};
use tetratto_core::model::{auth::IpBlock, communities::Question, ApiReturn, Error};
use crate::{get_user_from_token, routes::api::v1::CreateQuestion, State};
pub async fn create_request(
jar: CookieJar,
headers: HeaderMap,
Extension(data): Extension<State>,
Json(req): Json<CreateQuestion>,
) -> 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 user = get_user_from_token!(jar, data);
if req.is_global && user.is_none() {
return Json(Error::NotAllowed.into());
}
// get real ip
let real_ip = headers
.get(data.0.security.real_ip_header.to_owned())
.unwrap_or(&HeaderValue::from_static(""))
.to_str()
.unwrap_or("")
.to_string();
// check for ip ban
if data.get_ipban_by_ip(&real_ip).await.is_ok() {
return Json(Error::NotAllowed.into());
}
// ...
let mut props = Question::new(
user.id,
if let Some(ref ua) = user { ua.id } else { 0 },
match req.receiver.parse::<usize>() {
Ok(x) => x,
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
},
req.content,
req.is_global,
real_ip,
);
if !req.community.is_empty() {
@ -63,3 +85,43 @@ pub async fn delete_request(
Err(e) => Json(e.into()),
}
}
pub async fn 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) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
// get question
let question = match data.get_question_by_id(id).await {
Ok(q) => q,
Err(e) => return Json(e.into()),
};
// check for an existing ip block
if data
.get_ipblock_by_initiator_receiver(user.id, &question.ip)
.await
.is_ok()
{
return Json(Error::NotAllowed.into());
}
// create ip block
match data
.create_ipblock(IpBlock::new(user.id, question.ip))
.await
{
Ok(_) => Json(ApiReturn {
ok: true,
message: "IP blocked".to_string(),
payload: (),
}),
Err(e) => Json(e.into()),
}
}

View file

@ -100,6 +100,10 @@ pub fn routes() -> Router {
"/questions/{id}",
delete(communities::questions::delete_request),
)
.route(
"/questions/{id}/block_ip",
post(communities::questions::ip_block_request),
)
// auth
// global
.route("/auth/register", post(auth::register_request))
@ -177,6 +181,7 @@ pub fn routes() -> Router {
"/auth/user/find_by_ip/{ip}",
get(auth::profile::redirect_from_ip),
)
.route("/auth/ip/{ip}/block", post(auth::social::ip_block_request))
// warnings
.route("/warnings/{id}", post(auth::user_warnings::create_request))
.route(

View file

@ -20,6 +20,10 @@ pub fn routes(config: &Config) -> Router {
get_service(tower_http::services::ServeDir::new(&config.dirs.assets)),
)
.route("/public/favicon.svg", get(assets::favicon_request))
.route_service(
"/robots.txt",
tower_http::services::ServeFile::new(format!("{}/robots.txt", config.dirs.assets)),
)
// api
.nest("/api/v1", api::v1::routes())
// pages