diff --git a/crates/app/src/public/html/communities/list.html b/crates/app/src/public/html/communities/list.html
index e5cb525..ee15f87 100644
--- a/crates/app/src/public/html/communities/list.html
+++ b/crates/app/src/public/html/communities/list.html
@@ -68,6 +68,11 @@
async function create_community_from_form(e) {
e.preventDefault();
await trigger("atto::debounce", ["communities::create"]);
+
+ if (e.target.title.value.includes(" ")) {
+ return alert("Cannot contain spaces!");
+ }
+
fetch("/api/v1/communities", {
method: "POST",
headers: {
diff --git a/crates/app/src/public/html/communities/settings.html b/crates/app/src/public/html/communities/settings.html
index 7a4a537..4fc28bd 100644
--- a/crates/app/src/public/html/communities/settings.html
+++ b/crates/app/src/public/html/communities/settings.html
@@ -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) => {
e.preventDefault();
fetch(
@@ -359,6 +386,7 @@
${res.payload.role !== 33 ? `` : ``}
${res.payload.role !== 65 ? `` : ``}
+
diff --git a/crates/app/src/routes/api/v1/communities/communities.rs b/crates/app/src/routes/api/v1/communities/communities.rs
index ba4eb89..10bf691 100644
--- a/crates/app/src/routes/api/v1/communities/communities.rs
+++ b/crates/app/src/routes/api/v1/communities/communities.rs
@@ -12,12 +12,13 @@ use tetratto_core::model::{
};
use crate::{
- State, get_user_from_token,
+ get_user_from_token,
routes::api::v1::{
- CreateCommunity, UpdateCommunityContext, UpdateCommunityJoinAccess,
+ CreateCommunity, UpdateCommunityContext, UpdateCommunityJoinAccess, UpdateCommunityOwner,
UpdateCommunityReadAccess, UpdateCommunityTitle, UpdateCommunityWriteAccess,
UpdateMembershipRole,
},
+ State,
};
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,
+ Path(id): Path,
+ Json(req): Json,
+) -> 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::() {
+ 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(
jar: CookieJar,
Extension(data): Extension,
diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs
index 84e49e2..c0135f4 100644
--- a/crates/app/src/routes/api/v1/mod.rs
+++ b/crates/app/src/routes/api/v1/mod.rs
@@ -64,6 +64,10 @@ pub fn routes() -> Router {
"/communities/{id}/access/join",
post(communities::communities::update_join_access_request),
)
+ .route(
+ "/communities/{id}/transfer_ownership",
+ post(communities::communities::update_owner_request),
+ )
.route(
"/communities/{id}/upload/avatar",
post(communities::images::upload_avatar_request),
@@ -342,6 +346,11 @@ pub struct UpdateMembershipRole {
pub role: CommunityPermission,
}
+#[derive(Deserialize)]
+pub struct UpdateCommunityOwner {
+ pub user: String,
+}
+
#[derive(Deserialize)]
pub struct UpdateUserRole {
pub role: FinePermission,
diff --git a/crates/core/src/database/communities.rs b/crates/core/src/database/communities.rs
index 33accc3..2f0e6ff 100644
--- a/crates/core/src/database/communities.rs
+++ b/crates/core/src/database/communities.rs
@@ -397,6 +397,68 @@ impl DataManager {
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_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);