Removing the site creator, adding leave_admin. Fixes #1808 (#2052)

* Removing the site creator, adding leave_admin. Fixes #1808

* Making sure there's at least one admin. Fixing unit tests
This commit is contained in:
Dessalines 2022-01-26 12:57:16 -05:00 committed by GitHub
parent 1372827b41
commit e36ad9d984
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 63 additions and 116 deletions

View File

@ -44,7 +44,6 @@ use lemmy_db_schema::{
},
person::Person,
post::Post,
site::Site,
},
traits::{Bannable, Blockable, Crud, Followable, Joinable},
};
@ -457,20 +456,7 @@ impl Perform for TransferCommunity {
let local_user_view =
get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
let site_creator_id = blocking(context.pool(), move |conn| {
Site::read(conn, 1).map(|s| s.creator_id)
})
.await??;
let mut admins = blocking(context.pool(), PersonViewSafe::admins).await??;
// Making sure the site creator, if an admin, is at the top
let creator_index = admins
.iter()
.position(|r| r.person.id == site_creator_id)
.context(location_info!())?;
let creator_person = admins.remove(creator_index);
admins.insert(0, creator_person);
let admins = blocking(context.pool(), PersonViewSafe::admins).await??;
// Fetch the community mods
let community_id = data.community_id;

View File

@ -111,9 +111,7 @@ pub async fn match_websocket_operation(
UserOperation::TransferCommunity => {
do_websocket_operation::<TransferCommunity>(context, id, op, data).await
}
UserOperation::TransferSite => {
do_websocket_operation::<TransferSite>(context, id, op, data).await
}
UserOperation::LeaveAdmin => do_websocket_operation::<LeaveAdmin>(context, id, op, data).await,
// Community ops
UserOperation::FollowCommunity => {

View File

@ -1,6 +1,5 @@
use crate::{captcha_as_wav_base64, Perform};
use actix_web::web::Data;
use anyhow::Context;
use bcrypt::verify;
use captcha::{gen, Difficulty};
use chrono::Duration;
@ -51,7 +50,6 @@ use lemmy_db_views_actor::{
};
use lemmy_utils::{
claims::Claims,
location_info,
utils::{is_valid_display_name, is_valid_matrix_id, naive_from_unix},
ConnectionId,
LemmyError,
@ -409,18 +407,7 @@ impl Perform for AddAdmin {
blocking(context.pool(), move |conn| ModAdd::create(conn, &form)).await??;
let site_creator_id = blocking(context.pool(), move |conn| {
Site::read(conn, 1).map(|s| s.creator_id)
})
.await??;
let mut admins = blocking(context.pool(), PersonViewSafe::admins).await??;
let creator_index = admins
.iter()
.position(|r| r.person.id == site_creator_id)
.context(location_info!())?;
let creator_person = admins.remove(creator_index);
admins.insert(0, creator_person);
let admins = blocking(context.pool(), PersonViewSafe::admins).await??;
let res = AddAdminResponse { admins };

View File

@ -1,6 +1,5 @@
use crate::Perform;
use actix_web::web::Data;
use anyhow::Context;
use diesel::NotFound;
use lemmy_api_common::{
blocking,
@ -27,6 +26,7 @@ use lemmy_db_schema::{
source::{
local_user::{LocalUser, LocalUserForm},
moderator::*,
person::Person,
registration_application::{RegistrationApplication, RegistrationApplicationForm},
site::Site,
},
@ -62,7 +62,7 @@ use lemmy_db_views_moderator::{
mod_sticky_post_view::ModStickyPostView,
mod_transfer_community_view::ModTransferCommunityView,
};
use lemmy_utils::{location_info, settings::structs::Settings, version, ConnectionId, LemmyError};
use lemmy_utils::{settings::structs::Settings, version, ConnectionId, LemmyError};
use lemmy_websocket::LemmyContext;
#[async_trait::async_trait(?Send)]
@ -462,7 +462,7 @@ async fn convert_response(
}
#[async_trait::async_trait(?Send)]
impl Perform for TransferSite {
impl Perform for LeaveAdmin {
type Response = GetSiteResponse;
#[tracing::instrument(skip(context, _websocket_id))]
@ -471,44 +471,36 @@ impl Perform for TransferSite {
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<GetSiteResponse, LemmyError> {
let data: &TransferSite = self;
let data: &LeaveAdmin = self;
let local_user_view =
get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
is_admin(&local_user_view)?;
let read_site = blocking(context.pool(), Site::read_simple).await??;
// Make sure user is the creator
if read_site.creator_id != local_user_view.person.id {
return Err(LemmyError::from_message("not_an_admin"));
// Make sure there isn't just one admin (so if one leaves, there will still be one left)
let admins = blocking(context.pool(), PersonViewSafe::admins).await??;
if admins.len() == 1 {
return Err(LemmyError::from_message("cannot_leave_admin"));
}
let new_creator_id = data.person_id;
let transfer_site = move |conn: &'_ _| Site::transfer(conn, new_creator_id);
blocking(context.pool(), transfer_site)
.await?
.map_err(LemmyError::from)
.map_err(|e| e.with_message("couldnt_update_site"))?;
let person_id = local_user_view.person.id;
blocking(context.pool(), move |conn| {
Person::leave_admin(conn, person_id)
})
.await??;
// Mod tables
let form = ModAddForm {
mod_person_id: local_user_view.person.id,
other_person_id: data.person_id,
removed: Some(false),
mod_person_id: person_id,
other_person_id: person_id,
removed: Some(true),
};
blocking(context.pool(), move |conn| ModAdd::create(conn, &form)).await??;
// Reread site and admins
let site_view = blocking(context.pool(), SiteView::read).await??;
let mut admins = blocking(context.pool(), PersonViewSafe::admins).await??;
let creator_index = admins
.iter()
.position(|r| r.person.id == site_view.creator.id)
.context(location_info!())?;
let creator_person = admins.remove(creator_index);
admins.insert(0, creator_person);
let admins = blocking(context.pool(), PersonViewSafe::admins).await??;
let federated_instances = build_federated_instances(
context.pool(),

View File

@ -155,8 +155,7 @@ pub struct MyUserInfo {
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TransferSite {
pub person_id: PersonId,
pub struct LeaveAdmin {
pub auth: Sensitive<String>,
}

View File

@ -62,7 +62,6 @@ impl PerformCrud for CreateSite {
description,
icon,
banner,
creator_id: local_user_view.person.id,
enable_downvotes: data.enable_downvotes,
open_registration: data.open_registration,
enable_nsfw: data.enable_nsfw,

View File

@ -79,18 +79,7 @@ impl PerformCrud for GetSite {
}
};
let mut admins = blocking(context.pool(), PersonViewSafe::admins).await??;
// Make sure the site creator is the top admin
if let Some(site_view) = site_view.to_owned() {
let site_creator_id = site_view.creator.id;
// TODO investigate why this is sometimes coming back null
// Maybe user_.admin isn't being set to true?
if let Some(creator_index) = admins.iter().position(|r| r.person.id == site_creator_id) {
let creator_person = admins.remove(creator_index);
admins.insert(0, creator_person);
}
}
let admins = blocking(context.pool(), PersonViewSafe::admins).await??;
let online = context
.chat_server()

View File

@ -54,7 +54,6 @@ impl PerformCrud for EditSite {
}
let site_form = SiteForm {
creator_id: found_site.creator_id,
name: data.name.to_owned().unwrap_or(found_site.name),
sidebar,
description,

View File

@ -55,7 +55,6 @@ mod tests {
let site_form = SiteForm {
name: "test_site".into(),
creator_id: inserted_person.id,
sidebar: None,
description: None,
icon: None,
@ -133,7 +132,12 @@ mod tests {
let community_num_deleted = Community::delete(&conn, inserted_community.id).unwrap();
assert_eq!(1, community_num_deleted);
let after_delete = SiteAggregates::read(&conn);
assert!(after_delete.is_err());
// Site should still exist, it can without a site creator.
let after_delete_creator = SiteAggregates::read(&conn);
assert!(after_delete_creator.is_ok());
Site::delete(&conn, 1).unwrap();
let after_delete_site = SiteAggregates::read(&conn);
assert!(after_delete_site.is_err());
}
}

View File

@ -274,6 +274,12 @@ impl Person {
pub fn is_banned(&self) -> bool {
is_banned(self.banned, self.ban_expires)
}
pub fn leave_admin(conn: &PgConnection, person_id: PersonId) -> Result<Self, Error> {
diesel::update(person.find(person_id))
.set(admin.eq(false))
.get_result::<Self>(conn)
}
}
impl PersonSafe {

View File

@ -1,4 +1,4 @@
use crate::{naive_now, newtypes::PersonId, source::site::*, traits::Crud};
use crate::{source::site::*, traits::Crud};
use diesel::{dsl::*, result::Error, *};
impl Crud for Site {
@ -27,13 +27,6 @@ impl Crud for Site {
}
impl Site {
pub fn transfer(conn: &PgConnection, new_creator_id: PersonId) -> Result<Site, Error> {
use crate::schema::site::dsl::*;
diesel::update(site.find(1))
.set((creator_id.eq(new_creator_id), updated.eq(naive_now())))
.get_result::<Self>(conn)
}
pub fn read_simple(conn: &PgConnection) -> Result<Self, Error> {
use crate::schema::site::dsl::*;
site.first::<Self>(conn)

View File

@ -441,7 +441,6 @@ table! {
id -> Int4,
name -> Varchar,
sidebar -> Nullable<Text>,
creator_id -> Int4,
published -> Timestamp,
updated -> Nullable<Timestamp>,
enable_downvotes -> Bool,
@ -648,7 +647,6 @@ joinable!(post_read -> post (post_id));
joinable!(post_report -> post (post_id));
joinable!(post_saved -> person (person_id));
joinable!(post_saved -> post (post_id));
joinable!(site -> person (creator_id));
joinable!(site_aggregates -> site (site_id));
joinable!(email_verification -> local_user (local_user_id));
joinable!(registration_application -> local_user (local_user_id));

View File

@ -1,7 +1,4 @@
use crate::{
newtypes::{DbUrl, PersonId},
schema::site,
};
use crate::{newtypes::DbUrl, schema::site};
use serde::{Deserialize, Serialize};
#[derive(Queryable, Identifiable, PartialEq, Debug, Clone, Serialize, Deserialize)]
@ -10,7 +7,6 @@ pub struct Site {
pub id: i32,
pub name: String,
pub sidebar: Option<String>,
pub creator_id: PersonId,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub enable_downvotes: bool,
@ -30,7 +26,6 @@ pub struct Site {
#[table_name = "site"]
pub struct SiteForm {
pub name: String,
pub creator_id: PersonId,
pub sidebar: Option<Option<String>>,
pub updated: Option<chrono::NaiveDateTime>,
pub enable_downvotes: Option<bool>,

View File

@ -1,38 +1,24 @@
use diesel::{result::Error, *};
use lemmy_db_schema::{
aggregates::site_aggregates::SiteAggregates,
schema::{person, site, site_aggregates},
source::{
person::{Person, PersonSafe},
site::Site,
},
traits::ToSafe,
schema::{site, site_aggregates},
source::site::Site,
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SiteView {
pub site: Site,
pub creator: PersonSafe,
pub counts: SiteAggregates,
}
impl SiteView {
pub fn read(conn: &PgConnection) -> Result<Self, Error> {
let (site, creator, counts) = site::table
.inner_join(person::table)
let (site, counts) = site::table
.inner_join(site_aggregates::table)
.select((
site::all_columns,
Person::safe_columns_tuple(),
site_aggregates::all_columns,
))
.first::<(Site, PersonSafe, SiteAggregates)>(conn)?;
.select((site::all_columns, site_aggregates::all_columns))
.first::<(Site, SiteAggregates)>(conn)?;
Ok(SiteView {
site,
creator,
counts,
})
Ok(SiteView { site, counts })
}
}

View File

@ -136,7 +136,7 @@ pub enum UserOperation {
MarkAllAsRead,
SaveUserSettings,
TransferCommunity,
TransferSite,
LeaveAdmin,
PasswordReset,
PasswordChange,
MarkPrivateMessageAsRead,

View File

@ -0,0 +1,14 @@
-- Add the column back
alter table site add column creator_id int references person on update cascade on delete cascade;
-- Add the data, selecting the highest admin
update site
set creator_id = sub.id
from (
select id from person
where admin = true
limit 1
) as sub;
-- Set to not null
alter table site alter column creator_id set not null;

View File

@ -0,0 +1,2 @@
-- Drop the column
alter table site drop column creator_id;

View File

@ -19,7 +19,6 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
// Admin Actions
.route("", web::post().to(route_post_crud::<CreateSite>))
.route("", web::put().to(route_post_crud::<EditSite>))
.route("/transfer", web::post().to(route_post::<TransferSite>))
.route("/config", web::get().to(route_get::<GetSiteConfig>))
.route("/config", web::put().to(route_post::<SaveSiteConfig>)),
)
@ -212,7 +211,8 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
)
.route("/report_count", web::get().to(route_get::<GetReportCount>))
.route("/unread_count", web::get().to(route_get::<GetUnreadCount>))
.route("/verify_email", web::post().to(route_post::<VerifyEmail>)),
.route("/verify_email", web::post().to(route_post::<VerifyEmail>))
.route("/leave_admin", web::post().to(route_post::<LeaveAdmin>)),
)
// Admin Actions
.service(