add: ability to transfer community ownership

This commit is contained in:
trisua 2025-04-24 16:57:25 -04:00
parent d42375441f
commit 0c814e95d7
5 changed files with 139 additions and 2 deletions

View file

@ -68,6 +68,11 @@
async function create_community_from_form(e) { async function create_community_from_form(e) {
e.preventDefault(); e.preventDefault();
await trigger("atto::debounce", ["communities::create"]); await trigger("atto::debounce", ["communities::create"]);
if (e.target.title.value.includes(" ")) {
return alert("Cannot contain spaces!");
}
fetch("/api/v1/communities", { fetch("/api/v1/communities", {
method: "POST", method: "POST",
headers: { headers: {

View file

@ -317,6 +317,33 @@
}); });
}; };
globalThis.transfer_ownership = async (uid) => {
if (
!(await trigger("atto::confirm", [
"Are you sure you would like to do this?\n\nThis action is PERMANENT!",
]))
) {
return;
}
fetch(`/api/v1/communities/{{ community.id }}/transfer_ownership`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user: uid,
}),
})
.then((res) => res.json())
.then((res) => {
trigger("atto::toast", [
res.ok ? "success" : "error",
res.message,
]);
});
};
globalThis.select_user_from_form = (e) => { globalThis.select_user_from_form = (e) => {
e.preventDefault(); e.preventDefault();
fetch( fetch(
@ -359,6 +386,7 @@
${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 !== 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>`} ${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> <button class="red quaternary" onclick="kick_user('${e.target.uid.value}')">Kick</button>
<button class="red quaternary" onclick="transfer_ownership('${e.target.uid.value}')">Transfer ownership</button>
</div> </div>
<div class="flex flex-col gap-2" ui_ident="permissions" id="permissions"> <div class="flex flex-col gap-2" ui_ident="permissions" id="permissions">

View file

@ -12,12 +12,13 @@ use tetratto_core::model::{
}; };
use crate::{ use crate::{
State, get_user_from_token, get_user_from_token,
routes::api::v1::{ routes::api::v1::{
CreateCommunity, UpdateCommunityContext, UpdateCommunityJoinAccess, CreateCommunity, UpdateCommunityContext, UpdateCommunityJoinAccess, UpdateCommunityOwner,
UpdateCommunityReadAccess, UpdateCommunityTitle, UpdateCommunityWriteAccess, UpdateCommunityReadAccess, UpdateCommunityTitle, UpdateCommunityWriteAccess,
UpdateMembershipRole, UpdateMembershipRole,
}, },
State,
}; };
pub async fn redirect_from_id( pub async fn redirect_from_id(
@ -201,6 +202,38 @@ pub async fn update_join_access_request(
} }
} }
pub async fn update_owner_request(
jar: CookieJar,
Extension(data): Extension<State>,
Path(id): Path<usize>,
Json(req): Json<UpdateCommunityOwner>,
) -> 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_community_owner(
id,
user,
match req.user.parse::<usize>() {
Ok(x) => x,
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
},
)
.await
{
Ok(_) => Json(ApiReturn {
ok: true,
message: "Community updated".to_string(),
payload: (),
}),
Err(e) => Json(e.into()),
}
}
pub async fn get_membership( pub async fn get_membership(
jar: CookieJar, jar: CookieJar,
Extension(data): Extension<State>, Extension(data): Extension<State>,

View file

@ -64,6 +64,10 @@ pub fn routes() -> Router {
"/communities/{id}/access/join", "/communities/{id}/access/join",
post(communities::communities::update_join_access_request), post(communities::communities::update_join_access_request),
) )
.route(
"/communities/{id}/transfer_ownership",
post(communities::communities::update_owner_request),
)
.route( .route(
"/communities/{id}/upload/avatar", "/communities/{id}/upload/avatar",
post(communities::images::upload_avatar_request), post(communities::images::upload_avatar_request),
@ -342,6 +346,11 @@ pub struct UpdateMembershipRole {
pub role: CommunityPermission, pub role: CommunityPermission,
} }
#[derive(Deserialize)]
pub struct UpdateCommunityOwner {
pub user: String,
}
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct UpdateUserRole { pub struct UpdateUserRole {
pub role: FinePermission, pub role: FinePermission,

View file

@ -397,6 +397,68 @@ impl DataManager {
Ok(()) Ok(())
} }
pub async fn update_community_owner(
&self,
id: usize,
user: User,
new_owner: usize,
) -> Result<()> {
let y = self.get_community_by_id(id).await?;
if user.id != y.owner {
if !user.permissions.check(FinePermission::MANAGE_COMMUNITIES) {
return Err(Error::NotAllowed);
} else {
self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new(
user.id,
format!("invoked `update_community_owner` with x value `{id}`"),
))
.await?
}
}
let new_owner_membership = self
.get_membership_by_owner_community(new_owner, y.id)
.await?;
let current_owner_membership = self
.get_membership_by_owner_community(y.owner, y.id)
.await?;
// ...
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = execute!(
&conn,
"UPDATE communities SET owner = $1 WHERE id = $2",
params![&(new_owner as i64), &(id as i64)]
);
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
self.cache_clear_community(&y).await;
// update memberships
self.update_membership_role(
new_owner_membership.id,
CommunityPermission::DEFAULT | CommunityPermission::ADMINISTRATOR,
)
.await?;
self.update_membership_role(
current_owner_membership.id,
CommunityPermission::DEFAULT | CommunityPermission::MEMBER,
)
.await?;
// return
Ok(())
}
auto_method!(update_community_context(CommunityContext)@get_community_by_id_no_void:MANAGE_COMMUNITIES -> "UPDATE communities SET context = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_community); auto_method!(update_community_context(CommunityContext)@get_community_by_id_no_void:MANAGE_COMMUNITIES -> "UPDATE communities SET context = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_community);
auto_method!(update_community_read_access(CommunityReadAccess)@get_community_by_id_no_void:MANAGE_COMMUNITIES -> "UPDATE communities SET read_access = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_community); auto_method!(update_community_read_access(CommunityReadAccess)@get_community_by_id_no_void:MANAGE_COMMUNITIES -> "UPDATE communities SET read_access = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_community);
auto_method!(update_community_write_access(CommunityWriteAccess)@get_community_by_id_no_void:MANAGE_COMMUNITIES -> "UPDATE communities SET write_access = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_community); auto_method!(update_community_write_access(CommunityWriteAccess)@get_community_by_id_no_void:MANAGE_COMMUNITIES -> "UPDATE communities SET write_access = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_community);