From 03db5b1ddf0f7eda6ac2dabf2a7905fd82cd1baa Mon Sep 17 00:00:00 2001 From: IAmTomahawkx Date: Sun, 8 Jun 2025 01:18:07 -0700 Subject: [PATCH 1/4] Addd admin models to database --- crates/core/config/Revolt.toml | 4 + crates/core/config/src/lib.rs | 2 + crates/core/database/src/drivers/reference.rs | 19 +++- .../database/src/models/admin_audits/mod.rs | 5 + .../src/models/admin_audits/models.rs | 17 +++ .../database/src/models/admin_audits/ops.rs | 17 +++ .../src/models/admin_audits/ops/mongodb.rs | 48 ++++++++ .../src/models/admin_audits/ops/reference.rs | 45 ++++++++ .../src/models/admin_case_comments/mod.rs | 5 + .../src/models/admin_case_comments/models.rs | 16 +++ .../src/models/admin_case_comments/ops.rs | 19 ++++ .../models/admin_case_comments/ops/mongodb.rs | 36 ++++++ .../admin_case_comments/ops/reference.rs | 49 +++++++++ .../database/src/models/admin_cases/mod.rs | 5 + .../database/src/models/admin_cases/models.rs | 21 ++++ .../database/src/models/admin_cases/ops.rs | 28 +++++ .../src/models/admin_cases/ops/mongodb.rs | 80 ++++++++++++++ .../src/models/admin_cases/ops/reference.rs | 103 ++++++++++++++++++ .../database/src/models/admin_notes/mod.rs | 5 + .../database/src/models/admin_notes/models.rs | 25 +++++ .../database/src/models/admin_notes/ops.rs | 12 ++ .../src/models/admin_notes/ops/mongodb.rs | 34 ++++++ .../src/models/admin_notes/ops/reference.rs | 29 +++++ .../database/src/models/admin_strikes/mod.rs | 5 + .../src/models/admin_strikes/models.rs | 22 ++++ .../database/src/models/admin_strikes/ops.rs | 17 +++ .../src/models/admin_strikes/ops/mongodb.rs | 41 +++++++ .../src/models/admin_strikes/ops/reference.rs | 51 +++++++++ .../database/src/models/admin_tokens/axum.rs | 24 ++++ .../database/src/models/admin_tokens/mod.rs | 13 +++ .../src/models/admin_tokens/models.rs | 31 ++++++ .../database/src/models/admin_tokens/ops.rs | 14 +++ .../src/models/admin_tokens/ops/mongodb.rs | 30 +++++ .../src/models/admin_tokens/ops/reference.rs | 44 ++++++++ .../src/models/admin_tokens/rocket.rs | 37 +++++++ .../database/src/models/admin_users/axum.rs | 19 ++++ .../database/src/models/admin_users/mod.rs | 13 +++ .../database/src/models/admin_users/models.rs | 14 +++ .../database/src/models/admin_users/ops.rs | 16 +++ .../src/models/admin_users/ops/mongodb.rs | 27 +++++ .../src/models/admin_users/ops/reference.rs | 43 ++++++++ .../database/src/models/admin_users/rocket.rs | 39 +++++++ crates/core/database/src/models/mod.rs | 21 ++++ .../src/models/safety_reports/model.rs | 2 + .../core/database/src/models/users/model.rs | 24 ++++ crates/core/result/src/axum.rs | 2 + crates/core/result/src/lib.rs | 3 + crates/core/result/src/rocket.rs | 2 + 48 files changed, 1174 insertions(+), 4 deletions(-) create mode 100644 crates/core/database/src/models/admin_audits/mod.rs create mode 100644 crates/core/database/src/models/admin_audits/models.rs create mode 100644 crates/core/database/src/models/admin_audits/ops.rs create mode 100644 crates/core/database/src/models/admin_audits/ops/mongodb.rs create mode 100644 crates/core/database/src/models/admin_audits/ops/reference.rs create mode 100644 crates/core/database/src/models/admin_case_comments/mod.rs create mode 100644 crates/core/database/src/models/admin_case_comments/models.rs create mode 100644 crates/core/database/src/models/admin_case_comments/ops.rs create mode 100644 crates/core/database/src/models/admin_case_comments/ops/mongodb.rs create mode 100644 crates/core/database/src/models/admin_case_comments/ops/reference.rs create mode 100644 crates/core/database/src/models/admin_cases/mod.rs create mode 100644 crates/core/database/src/models/admin_cases/models.rs create mode 100644 crates/core/database/src/models/admin_cases/ops.rs create mode 100644 crates/core/database/src/models/admin_cases/ops/mongodb.rs create mode 100644 crates/core/database/src/models/admin_cases/ops/reference.rs create mode 100644 crates/core/database/src/models/admin_notes/mod.rs create mode 100644 crates/core/database/src/models/admin_notes/models.rs create mode 100644 crates/core/database/src/models/admin_notes/ops.rs create mode 100644 crates/core/database/src/models/admin_notes/ops/mongodb.rs create mode 100644 crates/core/database/src/models/admin_notes/ops/reference.rs create mode 100644 crates/core/database/src/models/admin_strikes/mod.rs create mode 100644 crates/core/database/src/models/admin_strikes/models.rs create mode 100644 crates/core/database/src/models/admin_strikes/ops.rs create mode 100644 crates/core/database/src/models/admin_strikes/ops/mongodb.rs create mode 100644 crates/core/database/src/models/admin_strikes/ops/reference.rs create mode 100644 crates/core/database/src/models/admin_tokens/axum.rs create mode 100644 crates/core/database/src/models/admin_tokens/mod.rs create mode 100644 crates/core/database/src/models/admin_tokens/models.rs create mode 100644 crates/core/database/src/models/admin_tokens/ops.rs create mode 100644 crates/core/database/src/models/admin_tokens/ops/mongodb.rs create mode 100644 crates/core/database/src/models/admin_tokens/ops/reference.rs create mode 100644 crates/core/database/src/models/admin_tokens/rocket.rs create mode 100644 crates/core/database/src/models/admin_users/axum.rs create mode 100644 crates/core/database/src/models/admin_users/mod.rs create mode 100644 crates/core/database/src/models/admin_users/models.rs create mode 100644 crates/core/database/src/models/admin_users/ops.rs create mode 100644 crates/core/database/src/models/admin_users/ops/mongodb.rs create mode 100644 crates/core/database/src/models/admin_users/ops/reference.rs create mode 100644 crates/core/database/src/models/admin_users/rocket.rs diff --git a/crates/core/config/Revolt.toml b/crates/core/config/Revolt.toml index e3a04f2a..fa9937bb 100644 --- a/crates/core/config/Revolt.toml +++ b/crates/core/config/Revolt.toml @@ -56,6 +56,9 @@ voso_legacy_token = "" trust_cloudflare = false # easypwned endpoint easypwned = "" +# admin api machine keys +# admin_keys = ["key_1", "key_2"] +admin_keys = [] [api.security.captcha] # hCaptcha configuration @@ -186,6 +189,7 @@ default_bucket = "revolt-uploads" [features] # Feature gate options webhooks_enabled = false +admin_api_enabled = false # Enable push notifications for mass pings (everyone, online, roles) # When false this will still ping in-client but will not send notifications from pushd mass_mentions_send_notifications = true diff --git a/crates/core/config/src/lib.rs b/crates/core/config/src/lib.rs index b4b3bed2..ebf480b6 100644 --- a/crates/core/config/src/lib.rs +++ b/crates/core/config/src/lib.rs @@ -171,6 +171,7 @@ pub struct ApiSecurity { pub captcha: ApiSecurityCaptcha, pub trust_cloudflare: bool, pub easypwned: String, + pub admin_keys: Vec, } #[derive(Deserialize, Debug, Clone)] @@ -333,6 +334,7 @@ pub struct Features { pub webhooks_enabled: bool, pub mass_mentions_send_notifications: bool, pub mass_mentions_enabled: bool, + pub admin_api_enabled: bool, #[serde(default)] pub advanced: FeaturesAdvanced, diff --git a/crates/core/database/src/drivers/reference.rs b/crates/core/database/src/drivers/reference.rs index ff18f253..6587b851 100644 --- a/crates/core/database/src/drivers/reference.rs +++ b/crates/core/database/src/drivers/reference.rs @@ -1,17 +1,28 @@ -use std::{collections::HashMap, sync::Arc}; +use std::{ + collections::{BTreeMap, HashMap}, + sync::Arc, +}; use futures::lock::Mutex; use crate::{ - Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, FileHash, Invite, Member, - MemberCompositeKey, Message, PolicyChange, RatelimitEvent, Report, Server, ServerBan, Snapshot, - User, UserSettings, Webhook, + AdminAuditItem, AdminCase, AdminCaseComment, AdminObjectNote, AdminStrike, AdminToken, + AdminUser, Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, FileHash, Invite, + Member, MemberCompositeKey, Message, PolicyChange, RatelimitEvent, Report, Server, ServerBan, + Snapshot, User, UserSettings, Webhook, }; database_derived!( /// Reference implementation #[derive(Default)] pub struct ReferenceDb { + pub admin_audits: Arc>>, + pub admin_cases: Arc>>, + pub admin_case_comments: Arc>>, + pub admin_object_notes: Arc>>, + pub admin_strikes: Arc>>, + pub admin_tokens: Arc>>, + pub admin_users: Arc>>, pub bots: Arc>>, pub channels: Arc>>, pub channel_invites: Arc>>, diff --git a/crates/core/database/src/models/admin_audits/mod.rs b/crates/core/database/src/models/admin_audits/mod.rs new file mode 100644 index 00000000..ead17df0 --- /dev/null +++ b/crates/core/database/src/models/admin_audits/mod.rs @@ -0,0 +1,5 @@ +mod models; +mod ops; + +pub use models::*; +pub use ops::*; diff --git a/crates/core/database/src/models/admin_audits/models.rs b/crates/core/database/src/models/admin_audits/models.rs new file mode 100644 index 00000000..8a7b2d35 --- /dev/null +++ b/crates/core/database/src/models/admin_audits/models.rs @@ -0,0 +1,17 @@ +auto_derived! { + pub struct AdminAuditItem { + /// The audit item ID + #[serde(rename = "_id")] + pub id: String, + /// The moderator who performed the action + pub mod_id: String, + /// The action performed (previously 'permission') + pub action: String, + /// The relevant case ID, if applicable + pub case_id: Option, + /// The object the action was taken against, if applicable + pub target: Option, + /// The context of the action, if applicable (eg. search phrases) + pub context: Option, + } +} diff --git a/crates/core/database/src/models/admin_audits/ops.rs b/crates/core/database/src/models/admin_audits/ops.rs new file mode 100644 index 00000000..adb68ef5 --- /dev/null +++ b/crates/core/database/src/models/admin_audits/ops.rs @@ -0,0 +1,17 @@ +mod mongodb; +mod reference; + +use revolt_result::Result; + +use crate::models::admin_audits::AdminAuditItem; + +#[async_trait] +pub trait AbstractAdminAudits: Sync + Send { + async fn admin_audit_insert(&self, audit: AdminAuditItem) -> Result<()>; + + async fn admin_audit_fetch( + &self, + before_id: Option<&str>, + limit: i64, + ) -> Result>; +} diff --git a/crates/core/database/src/models/admin_audits/ops/mongodb.rs b/crates/core/database/src/models/admin_audits/ops/mongodb.rs new file mode 100644 index 00000000..5689ec56 --- /dev/null +++ b/crates/core/database/src/models/admin_audits/ops/mongodb.rs @@ -0,0 +1,48 @@ +use async_std::stream::StreamExt; +use revolt_result::Result; + +use crate::AdminAuditItem; +use crate::MongoDb; + +use super::AbstractAdminAudits; + +static COL: &str = "admin_audits"; + +#[async_trait] +impl AbstractAdminAudits for MongoDb { + async fn admin_audit_insert(&self, audit: AdminAuditItem) -> Result<()> { + query!(self, insert_one, COL, audit).map(|_| ()) + } + + async fn admin_audit_fetch( + &self, + before_id: Option<&str>, + limit: i64, + ) -> Result> { + if let Some(before) = before_id { + Ok(self + .col::(COL) + .find(doc! { + "_id": { "$lt": before} + }) + .sort(doc! {"_id": -1}) + .limit(limit) + .await + .map_err(|_| create_database_error!("find", COL))? + .filter_map(|f| f.ok()) + .collect() + .await) + } else { + Ok(self + .col::(COL) + .find(doc! {}) + .sort(doc! {"_id": -1}) + .limit(limit) + .await + .map_err(|_| create_database_error!("find", COL))? + .filter_map(|f| f.ok()) + .collect() + .await) + } + } +} diff --git a/crates/core/database/src/models/admin_audits/ops/reference.rs b/crates/core/database/src/models/admin_audits/ops/reference.rs new file mode 100644 index 00000000..8c9c5ba8 --- /dev/null +++ b/crates/core/database/src/models/admin_audits/ops/reference.rs @@ -0,0 +1,45 @@ +use revolt_result::Result; + +use crate::AdminAuditItem; +use crate::ReferenceDb; + +use super::AbstractAdminAudits; + +#[async_trait] +impl AbstractAdminAudits for ReferenceDb { + async fn admin_audit_insert(&self, audit: AdminAuditItem) -> Result<()> { + let mut admin_audits = self.admin_audits.lock().await; + if admin_audits.contains_key(&audit.id) { + Err(create_database_error!("insert", "admin_audits")) + } else { + admin_audits.insert(audit.id.to_string(), audit.clone()); + Ok(()) + } + } + + async fn admin_audit_fetch( + &self, + before_id: Option<&str>, + limit: i64, + ) -> Result> { + let admin_audits = self.admin_audits.lock().await; + if let Some(before) = before_id { + Ok(admin_audits + .iter() + .rev() + .skip_while(|(id, _)| id.as_str() <= before) + .by_ref() + .take(limit as usize) + .map(|(_, item)| item.clone()) + .collect()) + } else { + Ok(admin_audits + .iter() + .rev() + .by_ref() + .take(limit as usize) + .map(|(_, item)| item.clone()) + .collect()) + } + } +} diff --git a/crates/core/database/src/models/admin_case_comments/mod.rs b/crates/core/database/src/models/admin_case_comments/mod.rs new file mode 100644 index 00000000..ead17df0 --- /dev/null +++ b/crates/core/database/src/models/admin_case_comments/mod.rs @@ -0,0 +1,5 @@ +mod models; +mod ops; + +pub use models::*; +pub use ops::*; diff --git a/crates/core/database/src/models/admin_case_comments/models.rs b/crates/core/database/src/models/admin_case_comments/models.rs new file mode 100644 index 00000000..8a54c2a7 --- /dev/null +++ b/crates/core/database/src/models/admin_case_comments/models.rs @@ -0,0 +1,16 @@ +auto_derived_partial! { + pub struct AdminCaseComment { + /// The comment ID + #[serde(rename = "_id")] + pub id: String, + /// The ID of the case this comment is attached to + pub case_id: String, + /// The author ID + pub user_id: String, + /// When the comment was edited, if applicable, in iso8601 + pub edited_at: Option, + /// The content + pub content: String + }, + "PartialAdminCaseComment" +} diff --git a/crates/core/database/src/models/admin_case_comments/ops.rs b/crates/core/database/src/models/admin_case_comments/ops.rs new file mode 100644 index 00000000..7f555a7d --- /dev/null +++ b/crates/core/database/src/models/admin_case_comments/ops.rs @@ -0,0 +1,19 @@ +mod mongodb; +mod reference; + +use revolt_result::Result; + +use crate::{models::admin_case_comments::AdminCaseComment, PartialAdminCaseComment}; + +#[async_trait] +pub trait AbstractAdminCaseComments: Sync + Send { + async fn admin_case_comment_insert(&self, comment: AdminCaseComment) -> Result<()>; + + async fn admin_case_comment_update( + &self, + comment_id: &str, + partial: &PartialAdminCaseComment, + ) -> Result<()>; + + async fn admin_case_comment_fetch(&self, case_id: &str) -> Result>; +} diff --git a/crates/core/database/src/models/admin_case_comments/ops/mongodb.rs b/crates/core/database/src/models/admin_case_comments/ops/mongodb.rs new file mode 100644 index 00000000..b01c0082 --- /dev/null +++ b/crates/core/database/src/models/admin_case_comments/ops/mongodb.rs @@ -0,0 +1,36 @@ +use revolt_result::Result; + +use crate::MongoDb; +use crate::{AdminCaseComment, PartialAdminCaseComment}; + +use super::AbstractAdminCaseComments; + +static COL: &str = "admin_case_comments"; + +#[async_trait] +impl AbstractAdminCaseComments for MongoDb { + async fn admin_case_comment_insert(&self, comment: AdminCaseComment) -> Result<()> { + query!(self, insert_one, COL, comment).map(|_| ()) + } + + async fn admin_case_comment_update( + &self, + comment_id: &str, + partial: &PartialAdminCaseComment, + ) -> Result<()> { + query!( + self, + update_one_by_id, + COL, + comment_id, + partial, + vec![], + None + ) + .map(|_| ()) + } + + async fn admin_case_comment_fetch(&self, case_id: &str) -> Result> { + query!(self, find, COL, doc! {"case_id": case_id}) + } +} diff --git a/crates/core/database/src/models/admin_case_comments/ops/reference.rs b/crates/core/database/src/models/admin_case_comments/ops/reference.rs new file mode 100644 index 00000000..25570edf --- /dev/null +++ b/crates/core/database/src/models/admin_case_comments/ops/reference.rs @@ -0,0 +1,49 @@ +use iso8601_timestamp::Timestamp; +use revolt_result::Result; + +use crate::ReferenceDb; +use crate::{AdminCaseComment, PartialAdminCaseComment}; + +use super::AbstractAdminCaseComments; + +#[async_trait] +impl AbstractAdminCaseComments for ReferenceDb { + async fn admin_case_comment_insert(&self, comment: AdminCaseComment) -> Result<()> { + let mut admin_case_comments = self.admin_case_comments.lock().await; + if admin_case_comments.contains_key(&comment.id) { + Err(create_database_error!("insert", "admin_case_comments")) + } else { + admin_case_comments.insert(comment.id.to_string(), comment.clone()); + Ok(()) + } + } + + async fn admin_case_comment_update( + &self, + comment_id: &str, + partial: &PartialAdminCaseComment, + ) -> Result<()> { + let mut admin_case_comments = self.admin_case_comments.lock().await; + if let Some(comment) = admin_case_comments.get_mut(comment_id) { + comment.apply_options(partial.clone()); + comment.edited_at = Some(Timestamp::now_utc().format().to_string()); + Ok(()) + } else { + Err(create_error!(NotFound)) + } + } + + async fn admin_case_comment_fetch(&self, case_id: &str) -> Result> { + let admin_case_comments = self.admin_case_comments.lock().await; + Ok(admin_case_comments + .iter() + .filter_map(|(_, c)| { + if c.case_id == case_id { + Some(c.clone()) + } else { + None + } + }) + .collect()) + } +} diff --git a/crates/core/database/src/models/admin_cases/mod.rs b/crates/core/database/src/models/admin_cases/mod.rs new file mode 100644 index 00000000..ead17df0 --- /dev/null +++ b/crates/core/database/src/models/admin_cases/mod.rs @@ -0,0 +1,5 @@ +mod models; +mod ops; + +pub use models::*; +pub use ops::*; diff --git a/crates/core/database/src/models/admin_cases/models.rs b/crates/core/database/src/models/admin_cases/models.rs new file mode 100644 index 00000000..0764b139 --- /dev/null +++ b/crates/core/database/src/models/admin_cases/models.rs @@ -0,0 +1,21 @@ +auto_derived_partial! { + pub struct AdminCase { + /// The case ID + #[serde(rename = "_id")] + pub id: String, + /// The case Short ID + pub short_id: String, + + /// The owner of the case + pub owner_id: String, + /// The title of the case + pub title: String, + /// The status of the case (open/closed) + pub status: String, + /// When the case was closed, in iso8601 + pub closed_at: Option, + /// The tags for the case + pub tags: Vec, + }, + "PartialAdminCase" +} diff --git a/crates/core/database/src/models/admin_cases/ops.rs b/crates/core/database/src/models/admin_cases/ops.rs new file mode 100644 index 00000000..b13a062b --- /dev/null +++ b/crates/core/database/src/models/admin_cases/ops.rs @@ -0,0 +1,28 @@ +mod mongodb; +mod reference; +use revolt_result::Result; + +use crate::models::admin_cases::{AdminCase, PartialAdminCase}; + +#[async_trait] +pub trait AbstractAdminCases: Sync + Send { + async fn admin_case_create(&self, case: AdminCase) -> Result<()>; + + async fn admin_case_assign_report(&self, case_id: &str, report_id: &str) -> Result<()>; + + async fn admin_case_edit(&self, case_id: &str, partial: &PartialAdminCase) -> Result<()>; + + async fn admin_case_fetch(&self, case_id: &str) -> Result; + + /// title is fuzzy, the rest of the arguments are direct matches + /// before_id and limit are for paginating + async fn admin_case_search( + &self, + title: Option<&str>, + status: Option<&str>, + owner_id: Option<&str>, + tags: Option>, + before_id: Option<&str>, + limit: i64, + ) -> Result>; +} diff --git a/crates/core/database/src/models/admin_cases/ops/mongodb.rs b/crates/core/database/src/models/admin_cases/ops/mongodb.rs new file mode 100644 index 00000000..f2aa3ac7 --- /dev/null +++ b/crates/core/database/src/models/admin_cases/ops/mongodb.rs @@ -0,0 +1,80 @@ +use async_std::stream::StreamExt; +use bson::Document; +use revolt_result::Result; + +use crate::MongoDb; +use crate::{AdminCase, PartialAdminCase}; + +use super::AbstractAdminCases; + +static COL: &str = "admin_cases"; + +#[async_trait] +impl AbstractAdminCases for MongoDb { + async fn admin_case_create(&self, case: AdminCase) -> Result<()> { + query!(self, insert_one, COL, case).map(|_| ()) + } + + async fn admin_case_assign_report(&self, case_id: &str, report_id: &str) -> Result<()> { + self.col::("safety_reports") + .update_one( + doc! {"_id": { "$regex": format!("{}$", report_id)}}, + doc! {"case_id": case_id}, + ) + .await + .map(|_| ()) + .map_err(|_| create_database_error!("safety_reports", "update_one")) + } + + async fn admin_case_edit(&self, case_id: &str, partial: &PartialAdminCase) -> Result<()> { + query!(self, update_one_by_id, COL, case_id, partial, vec![], None).map(|_| ()) + } + + async fn admin_case_fetch(&self, case_id: &str) -> Result { + query!(self, find_one_by_id, COL, case_id)? + .ok_or_else(|| create_database_error!("find_one", COL)) + } + + /// title is fuzzy, the rest of the arguments are direct matches + async fn admin_case_search( + &self, + title: Option<&str>, + status: Option<&str>, + owner_id: Option<&str>, + tags: Option>, + before_id: Option<&str>, + limit: i64, + ) -> Result> { + let mut query = Document::new(); + + if let Some(title) = title { + query.insert("$text", doc! {"$search": title}); + } + + if let Some(status) = status { + query.insert("status", status); + } + + if let Some(owner) = owner_id { + query.insert("owner_id", owner); + } + + if let Some(tags) = tags { + query.insert("tags", doc! {"$elemMatch": {"$in": tags}}); + } + + if let Some(before) = before_id { + query.insert("_id", doc! {"$lt": before}); + } + + Ok(self + .col::(COL) + .find(query) + .limit(limit) + .await + .map_err(|_| create_database_error!("find", COL))? + .filter_map(|f| f.ok()) + .collect() + .await) + } +} diff --git a/crates/core/database/src/models/admin_cases/ops/reference.rs b/crates/core/database/src/models/admin_cases/ops/reference.rs new file mode 100644 index 00000000..a0d4657e --- /dev/null +++ b/crates/core/database/src/models/admin_cases/ops/reference.rs @@ -0,0 +1,103 @@ +use revolt_result::Result; + +use crate::ReferenceDb; +use crate::{AdminCase, PartialAdminCase}; + +use super::AbstractAdminCases; + +#[async_trait] +impl AbstractAdminCases for ReferenceDb { + async fn admin_case_create(&self, case: AdminCase) -> Result<()> { + let mut admin_cases = self.admin_cases.lock().await; + if admin_cases.contains_key(&case.id) { + Err(create_database_error!("insert", "admin_cases")) + } else { + admin_cases.insert(case.id.to_string(), case.clone()); + Ok(()) + } + } + + async fn admin_case_assign_report(&self, case_id: &str, report_id: &str) -> Result<()> { + let mut reports = self.safety_reports.lock().await; + if let Some(report) = reports.get_mut(report_id) { + report.case_id = Some(case_id.to_string()); + Ok(()) + } else { + Err(create_error!(NotFound)) + } + } + + async fn admin_case_edit(&self, case_id: &str, partial: &PartialAdminCase) -> Result<()> { + let mut admin_cases = self.admin_cases.lock().await; + if let Some(case) = admin_cases.get_mut(case_id) { + case.apply_options(partial.clone()); + Ok(()) + } else { + Err(create_error!(NotFound)) + } + } + + async fn admin_case_fetch(&self, case_id: &str) -> Result { + let admin_cases = self.admin_cases.lock().await; + if let Some(case) = admin_cases.get(case_id) { + Ok(case.clone()) + } else { + Err(create_error!(NotFound)) + } + } + + /// title is fuzzy, the rest of the arguments are direct matches + /// before_id and limit are for paginating + async fn admin_case_search( + &self, + title: Option<&str>, + status: Option<&str>, + owner_id: Option<&str>, + tags: Option>, + before_id: Option<&str>, + limit: i64, + ) -> Result> { + let admin_cases = self.admin_cases.lock().await; + Ok(admin_cases + .iter() + .filter(|(_, case)| { + if let Some(title) = title { + case.title.to_lowercase().contains(title) + } else { + true + } + }) + .filter(|(_, case)| { + if let Some(status) = status { + case.status == status + } else { + true + } + }) + .filter(|(_, case)| { + if let Some(owner) = owner_id { + case.owner_id == owner + } else { + true + } + }) + .filter(|(_, case)| { + if let Some(tags) = &tags { + tags.iter().filter(|p| case.tags.contains(p)).count() > 0 + } else { + true + } + }) + .skip_while(|(id, _)| { + if let Some(before) = before_id { + id.as_str() <= before + } else { + false + } + }) + .by_ref() + .take(limit as usize) + .map(|(_, case)| case.clone()) + .collect()) + } +} diff --git a/crates/core/database/src/models/admin_notes/mod.rs b/crates/core/database/src/models/admin_notes/mod.rs new file mode 100644 index 00000000..ead17df0 --- /dev/null +++ b/crates/core/database/src/models/admin_notes/mod.rs @@ -0,0 +1,5 @@ +mod models; +mod ops; + +pub use models::*; +pub use ops::*; diff --git a/crates/core/database/src/models/admin_notes/models.rs b/crates/core/database/src/models/admin_notes/models.rs new file mode 100644 index 00000000..ff3767c0 --- /dev/null +++ b/crates/core/database/src/models/admin_notes/models.rs @@ -0,0 +1,25 @@ +auto_derived_partial! { + pub struct AdminObjectNote { + /// The ID of the note, which is the same as the ID of the object it's attached to + #[serde(rename = "_id")] + pub id: String, + /// When the note was edited, in iso8601 + pub edited_at: String, + /// The last user to edit the note + pub last_edited_by_id: String, + /// The content of the note + pub content: String, + }, + "PartialAdminObjectNote" +} + +impl AdminObjectNote { + pub fn to_partial(&self) -> PartialAdminObjectNote { + PartialAdminObjectNote { + id: Some(self.id.clone()), + edited_at: Some(self.edited_at.clone()), + last_edited_by_id: Some(self.last_edited_by_id.clone()), + content: Some(self.content.clone()), + } + } +} diff --git a/crates/core/database/src/models/admin_notes/ops.rs b/crates/core/database/src/models/admin_notes/ops.rs new file mode 100644 index 00000000..d45da92f --- /dev/null +++ b/crates/core/database/src/models/admin_notes/ops.rs @@ -0,0 +1,12 @@ +mod mongodb; +mod reference; +use revolt_result::Result; + +use crate::models::admin_notes::AdminObjectNote; + +#[async_trait] +pub trait AbstractAdminNotes: Sync + Send { + async fn admin_note_update(&self, note: AdminObjectNote) -> Result<()>; + + async fn admin_note_fetch(&self, target_id: &str) -> Result; +} diff --git a/crates/core/database/src/models/admin_notes/ops/mongodb.rs b/crates/core/database/src/models/admin_notes/ops/mongodb.rs new file mode 100644 index 00000000..f34ff664 --- /dev/null +++ b/crates/core/database/src/models/admin_notes/ops/mongodb.rs @@ -0,0 +1,34 @@ +use revolt_result::Result; + +use crate::AdminObjectNote; +use crate::MongoDb; + +use super::AbstractAdminNotes; + +static COL: &str = "admin_notes"; + +#[async_trait] +impl AbstractAdminNotes for MongoDb { + async fn admin_note_update(&self, note: AdminObjectNote) -> Result<()> { + let resp: Result<()> = query!(self, insert_one, COL, note.clone()).map(|_| ()); + if resp.is_err() { + query!( + self, + update_one_by_id, + COL, + note.id.as_str(), + note.to_partial(), + vec![], + None + ) + .map(|_| ()) + } else { + Ok(()) + } + } + + async fn admin_note_fetch(&self, target_id: &str) -> Result { + query!(self, find_one_by_id, COL, target_id)? + .ok_or_else(|| create_database_error!("find_one", COL)) + } +} diff --git a/crates/core/database/src/models/admin_notes/ops/reference.rs b/crates/core/database/src/models/admin_notes/ops/reference.rs new file mode 100644 index 00000000..370b3744 --- /dev/null +++ b/crates/core/database/src/models/admin_notes/ops/reference.rs @@ -0,0 +1,29 @@ +use revolt_result::Result; + +use crate::AdminObjectNote; +use crate::ReferenceDb; + +use super::AbstractAdminNotes; + +#[async_trait] +impl AbstractAdminNotes for ReferenceDb { + async fn admin_note_update(&self, note: AdminObjectNote) -> Result<()> { + let mut admin_notes = self.admin_object_notes.lock().await; + if let Some(existing_note) = admin_notes.get_mut(¬e.id) { + existing_note.apply_options(note.to_partial()); + Ok(()) + } else { + admin_notes.insert(note.id.clone(), note); + Ok(()) + } + } + + async fn admin_note_fetch(&self, target_id: &str) -> Result { + let admin_notes = self.admin_object_notes.lock().await; + if let Some(note) = admin_notes.get(target_id) { + Ok(note.clone()) + } else { + Err(create_error!(NotFound)) + } + } +} diff --git a/crates/core/database/src/models/admin_strikes/mod.rs b/crates/core/database/src/models/admin_strikes/mod.rs new file mode 100644 index 00000000..ead17df0 --- /dev/null +++ b/crates/core/database/src/models/admin_strikes/mod.rs @@ -0,0 +1,5 @@ +mod models; +mod ops; + +pub use models::*; +pub use ops::*; diff --git a/crates/core/database/src/models/admin_strikes/models.rs b/crates/core/database/src/models/admin_strikes/models.rs new file mode 100644 index 00000000..3d8db55f --- /dev/null +++ b/crates/core/database/src/models/admin_strikes/models.rs @@ -0,0 +1,22 @@ +auto_derived_partial! { + pub struct AdminStrike { + /// The strike ID + #[serde(rename = "_id")] + pub id: String, + /// The object receiving the strike (user/server) + pub target_id: String, + /// The moderator who gave the strike + pub mod_id: String, + /// The case the strike was made under + pub case_id: Option, + /// Action associated with the strike (eg. suspension/ban) + pub associated_action: Option, + /// Has the strike been removed + pub overruled: bool, + /// The user-facing reason for the strike + pub reason: String, + /// Internal context for the strike + pub mod_context: Option, + }, + "PartialAdminStrike" +} diff --git a/crates/core/database/src/models/admin_strikes/ops.rs b/crates/core/database/src/models/admin_strikes/ops.rs new file mode 100644 index 00000000..a5f7c510 --- /dev/null +++ b/crates/core/database/src/models/admin_strikes/ops.rs @@ -0,0 +1,17 @@ +mod mongodb; +mod reference; +use revolt_result::Result; + +use crate::models::admin_strikes::models::{AdminStrike, PartialAdminStrike}; + +#[async_trait] +pub trait AbstractAdminStrikes: Sync + Send { + async fn admin_strike_insert(&self, strike: AdminStrike) -> Result<()>; + + async fn admin_strike_update(&self, strike_id: &str, partial: PartialAdminStrike) + -> Result<()>; + + async fn admin_strike_get(&self, strike_id: &str) -> Result; + + async fn admin_strike_get_user(&self, user_id: &str) -> Result>; +} diff --git a/crates/core/database/src/models/admin_strikes/ops/mongodb.rs b/crates/core/database/src/models/admin_strikes/ops/mongodb.rs new file mode 100644 index 00000000..4f465ac7 --- /dev/null +++ b/crates/core/database/src/models/admin_strikes/ops/mongodb.rs @@ -0,0 +1,41 @@ +use revolt_result::Result; + +use crate::MongoDb; +use crate::{AdminStrike, PartialAdminStrike}; + +use super::AbstractAdminStrikes; + +static COL: &str = "admin_strikes"; + +#[async_trait] +impl AbstractAdminStrikes for MongoDb { + async fn admin_strike_insert(&self, strike: AdminStrike) -> Result<()> { + query!(self, insert_one, COL, strike).map(|_| ()) + } + + async fn admin_strike_update( + &self, + strike_id: &str, + partial: PartialAdminStrike, + ) -> Result<()> { + query!( + self, + update_one_by_id, + COL, + strike_id, + partial, + vec![], + None + ) + .map(|_| ()) + } + + async fn admin_strike_get(&self, strike_id: &str) -> Result { + query!(self, find_one_by_id, COL, strike_id)? + .ok_or_else(|| create_database_error!("find_one", COL))? + } + + async fn admin_strike_get_user(&self, user_id: &str) -> Result> { + query!(self, find, COL, doc! {"target_id": user_id}) + } +} diff --git a/crates/core/database/src/models/admin_strikes/ops/reference.rs b/crates/core/database/src/models/admin_strikes/ops/reference.rs new file mode 100644 index 00000000..a630bdd1 --- /dev/null +++ b/crates/core/database/src/models/admin_strikes/ops/reference.rs @@ -0,0 +1,51 @@ +use revolt_result::Result; + +use crate::ReferenceDb; +use crate::{AdminStrike, PartialAdminStrike}; + +use super::AbstractAdminStrikes; + +#[async_trait] +impl AbstractAdminStrikes for ReferenceDb { + async fn admin_strike_insert(&self, strike: AdminStrike) -> Result<()> { + let mut admin_strikes = self.admin_strikes.lock().await; + if admin_strikes.contains_key(&strike.id) { + Err(create_database_error!("insert", "admin_strikes")) + } else { + admin_strikes.insert(strike.id.to_string(), strike.clone()); + Ok(()) + } + } + + async fn admin_strike_update( + &self, + strike_id: &str, + partial: PartialAdminStrike, + ) -> Result<()> { + let mut admin_strikes = self.admin_strikes.lock().await; + if let Some(strike) = admin_strikes.get_mut(strike_id) { + strike.apply_options(partial); + Ok(()) + } else { + Err(create_error!(NotFound)) + } + } + + async fn admin_strike_get(&self, strike_id: &str) -> Result { + let admin_strikes = self.admin_strikes.lock().await; + if let Some(strike) = admin_strikes.get(strike_id) { + Ok(strike.clone()) + } else { + Err(create_error!(NotFound)) + } + } + + async fn admin_strike_get_user(&self, user_id: &str) -> Result> { + let admin_strikes = self.admin_strikes.lock().await; + Ok(admin_strikes + .iter() + .filter(|(_, strike)| strike.target_id == user_id) + .map(|(_, strike)| strike.clone()) + .collect()) + } +} diff --git a/crates/core/database/src/models/admin_tokens/axum.rs b/crates/core/database/src/models/admin_tokens/axum.rs new file mode 100644 index 00000000..cf98fb68 --- /dev/null +++ b/crates/core/database/src/models/admin_tokens/axum.rs @@ -0,0 +1,24 @@ +use axum::{extract::FromRequestParts, http::request::Parts}; + +use revolt_result::{create_error, Error, Result}; + +use crate::{AdminMachineToken, Database}; + +#[async_trait::async_trait] +impl FromRequestParts for AdminMachineToken { + type Rejection = Error; + + async fn from_request_parts(parts: &mut Parts, _db: &Database) -> Result { + if let Some(Ok(token)) = parts.headers.get("x-admin-machine").map(|v| v.to_str()) { + let config = revolt_config::config().await; + let token = token.to_string(); + if config.api.security.admin_keys.contains(&token) { + Ok(AdminMachineToken::new()) + } else { + Err(create_error!(InvalidCredentials)) + } + } else { + Err(create_error!(NotAuthenticated)) + } + } +} diff --git a/crates/core/database/src/models/admin_tokens/mod.rs b/crates/core/database/src/models/admin_tokens/mod.rs new file mode 100644 index 00000000..7b1a1988 --- /dev/null +++ b/crates/core/database/src/models/admin_tokens/mod.rs @@ -0,0 +1,13 @@ +#[cfg(feature = "axum-impl")] +mod axum; +mod models; +mod ops; +#[cfg(feature = "rocket-impl")] +mod rocket; + +#[cfg(feature = "axum-impl")] +pub use self::axum::*; +#[cfg(feature = "rocket-impl")] +pub use self::rocket::*; +pub use models::*; +pub use ops::*; diff --git a/crates/core/database/src/models/admin_tokens/models.rs b/crates/core/database/src/models/admin_tokens/models.rs new file mode 100644 index 00000000..ce4c4a7a --- /dev/null +++ b/crates/core/database/src/models/admin_tokens/models.rs @@ -0,0 +1,31 @@ +auto_derived! { + pub struct AdminToken { + /// The token ID + #[serde(rename = "_id")] + pub id: String, + /// The user this token is attached to + pub user_id: String, + /// The token itself + pub token: String, + /// The expiry timestamp for this token, in iso6801 + pub expiry: String + } + + /// This struct is used to validate machine tokens when doing machine to machine communication. + pub struct AdminMachineToken { + /// Placeholder field. + pub valid: bool + } +} + +impl AdminMachineToken { + pub fn new() -> AdminMachineToken { + AdminMachineToken { valid: true } + } +} + +impl Default for AdminMachineToken { + fn default() -> Self { + AdminMachineToken::new() + } +} diff --git a/crates/core/database/src/models/admin_tokens/ops.rs b/crates/core/database/src/models/admin_tokens/ops.rs new file mode 100644 index 00000000..a42e73a3 --- /dev/null +++ b/crates/core/database/src/models/admin_tokens/ops.rs @@ -0,0 +1,14 @@ +mod mongodb; +mod reference; +use revolt_result::Result; + +use crate::models::admin_tokens::AdminToken; + +#[async_trait] +pub trait AbstractAdminTokens: Sync + Send { + async fn admin_token_create(&self, token: AdminToken) -> Result<()>; + + async fn admin_token_revoke(&self, token_id: &str) -> Result<()>; + + async fn admin_token_authenticate(&self, token: &str) -> Result; +} diff --git a/crates/core/database/src/models/admin_tokens/ops/mongodb.rs b/crates/core/database/src/models/admin_tokens/ops/mongodb.rs new file mode 100644 index 00000000..ee47bb3a --- /dev/null +++ b/crates/core/database/src/models/admin_tokens/ops/mongodb.rs @@ -0,0 +1,30 @@ +use revolt_result::Result; + +use crate::AdminToken; +use crate::MongoDb; + +use super::AbstractAdminTokens; + +static COL: &str = "admin_tokens"; + +#[async_trait] +impl AbstractAdminTokens for MongoDb { + async fn admin_token_create(&self, token: AdminToken) -> Result<()> { + query!(self, insert_one, COL, token).map(|_| ()) + } + + async fn admin_token_revoke(&self, token_id: &str) -> Result<()> { + query!(self, delete_one_by_id, COL, token_id).map(|k| { + if k.deleted_count > 0 { + Ok(()) + } else { + Err(create_error!(NotFound)) + } + })? + } + + async fn admin_token_authenticate(&self, token: &str) -> Result { + query!(self, find_one, COL, doc! {"token": token})? + .ok_or_else(|| create_error!(InvalidCredentials)) + } +} diff --git a/crates/core/database/src/models/admin_tokens/ops/reference.rs b/crates/core/database/src/models/admin_tokens/ops/reference.rs new file mode 100644 index 00000000..fd1697ee --- /dev/null +++ b/crates/core/database/src/models/admin_tokens/ops/reference.rs @@ -0,0 +1,44 @@ +use revolt_result::Result; + +use crate::AdminToken; +use crate::ReferenceDb; + +use super::AbstractAdminTokens; + +#[async_trait] +impl AbstractAdminTokens for ReferenceDb { + async fn admin_token_create(&self, token: AdminToken) -> Result<()> { + let mut admin_tokens = self.admin_tokens.lock().await; + if admin_tokens.contains_key(&token.id) { + Err(create_database_error!("insert", "admin_tokens")) + } else { + admin_tokens.insert(token.id.to_string(), token.clone()); + Ok(()) + } + } + + async fn admin_token_revoke(&self, token_id: &str) -> Result<()> { + let mut admin_tokens = self.admin_tokens.lock().await; + if admin_tokens.remove(token_id).is_some() { + Ok(()) + } else { + Err(create_error!(NotFound)) + } + } + + async fn admin_token_authenticate(&self, token: &str) -> Result { + let admin_tokens = self.admin_tokens.lock().await; + let result = admin_tokens + .iter() + .filter_map(|(_, t)| { + if t.token == token { + Some(t.clone()) + } else { + None + } + }) + .next(); + + Ok(result.ok_or_else(|| create_error!(NotFound))?) + } +} diff --git a/crates/core/database/src/models/admin_tokens/rocket.rs b/crates/core/database/src/models/admin_tokens/rocket.rs new file mode 100644 index 00000000..15e6078e --- /dev/null +++ b/crates/core/database/src/models/admin_tokens/rocket.rs @@ -0,0 +1,37 @@ +use rocket::http::Status; +use rocket::request::{self, FromRequest, Outcome, Request}; + +use crate::AdminMachineToken; + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for AdminMachineToken { + type Error = authifier::Error; + + async fn from_request(request: &'r Request<'_>) -> request::Outcome { + let user: &Option = request + .local_cache_async(async { + let config = revolt_config::config().await; + + let token = request + .headers() + .get("x-admin-machine") + .next() + .map(|x| x.to_string()); + + if let Some(token) = token { + if config.api.security.admin_keys.contains(&token) { + return Some(AdminMachineToken::new()); + } + } + + None + }) + .await; + + if let Some(user) = user { + Outcome::Success(user.clone()) + } else { + Outcome::Error((Status::Unauthorized, authifier::Error::InvalidSession)) + } + } +} diff --git a/crates/core/database/src/models/admin_users/axum.rs b/crates/core/database/src/models/admin_users/axum.rs new file mode 100644 index 00000000..f3dc8676 --- /dev/null +++ b/crates/core/database/src/models/admin_users/axum.rs @@ -0,0 +1,19 @@ +use axum::{extract::FromRequestParts, http::request::Parts}; + +use revolt_result::{create_error, Error, Result}; + +use crate::{AdminUser, Database}; + +#[async_trait::async_trait] +impl FromRequestParts for AdminUser { + type Rejection = Error; + + async fn from_request_parts(parts: &mut Parts, db: &Database) -> Result { + if let Some(Ok(token)) = parts.headers.get("x-admin-user").map(|v| v.to_str()) { + let session = db.admin_token_authenticate(token).await?; + db.admin_user_fetch(&session.user_id).await + } else { + Err(create_error!(NotAuthenticated)) + } + } +} diff --git a/crates/core/database/src/models/admin_users/mod.rs b/crates/core/database/src/models/admin_users/mod.rs new file mode 100644 index 00000000..7b1a1988 --- /dev/null +++ b/crates/core/database/src/models/admin_users/mod.rs @@ -0,0 +1,13 @@ +#[cfg(feature = "axum-impl")] +mod axum; +mod models; +mod ops; +#[cfg(feature = "rocket-impl")] +mod rocket; + +#[cfg(feature = "axum-impl")] +pub use self::axum::*; +#[cfg(feature = "rocket-impl")] +pub use self::rocket::*; +pub use models::*; +pub use ops::*; diff --git a/crates/core/database/src/models/admin_users/models.rs b/crates/core/database/src/models/admin_users/models.rs new file mode 100644 index 00000000..2242fe72 --- /dev/null +++ b/crates/core/database/src/models/admin_users/models.rs @@ -0,0 +1,14 @@ +auto_derived_partial! { + pub struct AdminUser { + /// The ID of the user + #[serde(rename = "_id")] + pub id: String, + /// The user's email + pub email: String, + /// Whether the user is active or not (ie. can they use the api) + pub active: bool, + /// The permissions of the user + pub permissions: u64, + }, + "PartialAdminUser" +} diff --git a/crates/core/database/src/models/admin_users/ops.rs b/crates/core/database/src/models/admin_users/ops.rs new file mode 100644 index 00000000..664aca4a --- /dev/null +++ b/crates/core/database/src/models/admin_users/ops.rs @@ -0,0 +1,16 @@ +mod mongodb; +mod reference; +use revolt_result::Result; + +use crate::models::admin_users::models::{AdminUser, PartialAdminUser}; + +#[async_trait] +pub trait AbstractAdminUsers: Sync + Send { + async fn admin_user_insert(&self, user: AdminUser) -> Result<()>; + + async fn admin_user_update(&self, user_id: &str, partial: PartialAdminUser) -> Result<()>; + + async fn admin_user_fetch(&self, user_id: &str) -> Result; + + async fn admin_user_list(&self) -> Result>; +} diff --git a/crates/core/database/src/models/admin_users/ops/mongodb.rs b/crates/core/database/src/models/admin_users/ops/mongodb.rs new file mode 100644 index 00000000..11513db5 --- /dev/null +++ b/crates/core/database/src/models/admin_users/ops/mongodb.rs @@ -0,0 +1,27 @@ +use revolt_result::Result; + +use crate::MongoDb; +use crate::{AdminUser, PartialAdminUser}; + +use super::AbstractAdminUsers; + +static COL: &str = "admin_users"; + +#[async_trait] +impl AbstractAdminUsers for MongoDb { + async fn admin_user_insert(&self, user: AdminUser) -> Result<()> { + query!(self, insert_one, COL, user).map(|_| ()) + } + + async fn admin_user_update(&self, user_id: &str, partial: PartialAdminUser) -> Result<()> { + query!(self, update_one_by_id, COL, user_id, partial, vec![], None).map(|_| ()) + } + + async fn admin_user_fetch(&self, user_id: &str) -> Result { + query!(self, find_one_by_id, COL, user_id)?.ok_or_else(|| create_error!(NotFound)) + } + + async fn admin_user_list(&self) -> Result> { + query!(self, find, COL, doc! {}) + } +} diff --git a/crates/core/database/src/models/admin_users/ops/reference.rs b/crates/core/database/src/models/admin_users/ops/reference.rs new file mode 100644 index 00000000..5bee2e81 --- /dev/null +++ b/crates/core/database/src/models/admin_users/ops/reference.rs @@ -0,0 +1,43 @@ +use revolt_result::Result; + +use crate::ReferenceDb; +use crate::{AdminUser, PartialAdminUser}; + +use super::AbstractAdminUsers; + +#[async_trait] +impl AbstractAdminUsers for ReferenceDb { + async fn admin_user_insert(&self, user: AdminUser) -> Result<()> { + let mut admin_users = self.admin_users.lock().await; + if admin_users.contains_key(&user.id) { + Err(create_database_error!("insert", "admin_users")) + } else { + admin_users.insert(user.id.to_string(), user.clone()); + Ok(()) + } + } + + async fn admin_user_update(&self, user_id: &str, partial: PartialAdminUser) -> Result<()> { + let mut admin_users = self.admin_users.lock().await; + if let Some(existing_user) = admin_users.get_mut(user_id) { + existing_user.apply_options(partial); + Ok(()) + } else { + Err(create_error!(NotFound)) + } + } + + async fn admin_user_fetch(&self, user_id: &str) -> Result { + let admin_users = self.admin_users.lock().await; + if let Some(user) = admin_users.get(user_id) { + Ok(user.clone()) + } else { + Err(create_error!(NotFound)) + } + } + + async fn admin_user_list(&self) -> Result> { + let admin_users = self.admin_users.lock().await; + Ok(admin_users.iter().map(|(_, u)| u).cloned().collect()) + } +} diff --git a/crates/core/database/src/models/admin_users/rocket.rs b/crates/core/database/src/models/admin_users/rocket.rs new file mode 100644 index 00000000..ca738b81 --- /dev/null +++ b/crates/core/database/src/models/admin_users/rocket.rs @@ -0,0 +1,39 @@ +use rocket::http::Status; +use rocket::request::{self, FromRequest, Outcome, Request}; + +use crate::{AdminUser, Database}; + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for AdminUser { + type Error = authifier::Error; + + async fn from_request(request: &'r Request<'_>) -> request::Outcome { + let user: &Option = request + .local_cache_async(async { + let db = request.rocket().state::().expect("`Database`"); + + let token = request + .headers() + .get("x-admin-user") + .next() + .map(|x| x.to_string()); + + if let Some(token) = token { + if let Ok(admin_token) = db.admin_token_authenticate(&token).await { + if let Ok(user) = db.admin_user_fetch(&admin_token.user_id).await { + return Some(user); + } + } + } + + None + }) + .await; + + if let Some(user) = user { + Outcome::Success(user.clone()) + } else { + Outcome::Error((Status::Unauthorized, authifier::Error::InvalidSession)) + } + } +} diff --git a/crates/core/database/src/models/mod.rs b/crates/core/database/src/models/mod.rs index 181a30c0..af990d57 100644 --- a/crates/core/database/src/models/mod.rs +++ b/crates/core/database/src/models/mod.rs @@ -1,4 +1,11 @@ +mod admin_audits; +mod admin_case_comments; +mod admin_cases; mod admin_migrations; +mod admin_notes; +mod admin_strikes; +mod admin_tokens; +mod admin_users; mod bots; mod channel_invites; mod channel_unreads; @@ -18,7 +25,14 @@ mod servers; mod user_settings; mod users; +pub use admin_audits::*; +pub use admin_case_comments::*; +pub use admin_cases::*; pub use admin_migrations::*; +pub use admin_notes::*; +pub use admin_strikes::*; +pub use admin_tokens::*; +pub use admin_users::*; pub use bots::*; pub use channel_invites::*; pub use channel_unreads::*; @@ -43,6 +57,13 @@ use crate::{Database, MongoDb, ReferenceDb}; pub trait AbstractDatabase: Sync + Send + + admin_audits::AbstractAdminAudits + + admin_case_comments::AbstractAdminCaseComments + + admin_cases::AbstractAdminCases + + admin_notes::AbstractAdminNotes + + admin_strikes::AbstractAdminStrikes + + admin_users::AbstractAdminUsers + + admin_tokens::AbstractAdminTokens + admin_migrations::AbstractMigrations + bots::AbstractBots + channels::AbstractChannels diff --git a/crates/core/database/src/models/safety_reports/model.rs b/crates/core/database/src/models/safety_reports/model.rs index 72fd28a6..cd6a32e4 100644 --- a/crates/core/database/src/models/safety_reports/model.rs +++ b/crates/core/database/src/models/safety_reports/model.rs @@ -18,5 +18,7 @@ auto_derived!( /// Additional notes included on the report #[serde(default)] pub notes: String, + /// The case this report is assigned to + pub case_id: Option, } ); diff --git a/crates/core/database/src/models/users/model.rs b/crates/core/database/src/models/users/model.rs index 0c2b6cee..cce0366c 100644 --- a/crates/core/database/src/models/users/model.rs +++ b/crates/core/database/src/models/users/model.rs @@ -825,3 +825,27 @@ impl User { badges } } + +pub struct UserFlagsValue(pub u32); + +impl UserFlagsValue { + pub fn has(&self, flag: UserFlags) -> bool { + self.has_value(flag as u32) + } + pub fn has_value(&self, bit: u32) -> bool { + let mask = 1 << bit; + self.0 & mask == mask + } + + pub fn set(&mut self, flag: UserFlags, toggle: bool) -> &mut Self { + self.set_value(flag as u32, toggle) + } + pub fn set_value(&mut self, bit: u32, toggle: bool) -> &mut Self { + if toggle { + self.0 |= 1 << bit; + } else { + self.0 &= !(1 << bit); + } + self + } +} diff --git a/crates/core/result/src/axum.rs b/crates/core/result/src/axum.rs index e4caf818..e9bee77a 100644 --- a/crates/core/result/src/axum.rs +++ b/crates/core/result/src/axum.rs @@ -84,6 +84,8 @@ impl IntoResponse for Error { ErrorType::FileTypeNotAllowed => StatusCode::BAD_REQUEST, ErrorType::ImageProcessingFailed => StatusCode::INTERNAL_SERVER_ERROR, ErrorType::NoEmbedData => StatusCode::BAD_REQUEST, + + ErrorType::ImATeaPot => StatusCode::IM_A_TEAPOT, }; (status, Json(&self)).into_response() diff --git a/crates/core/result/src/lib.rs b/crates/core/result/src/lib.rs index 10b56d5d..6e58fcab 100644 --- a/crates/core/result/src/lib.rs +++ b/crates/core/result/src/lib.rs @@ -175,6 +175,9 @@ pub enum ErrorType { FeatureDisabled { feature: String, }, + + // :) + ImATeaPot, } #[macro_export] diff --git a/crates/core/result/src/rocket.rs b/crates/core/result/src/rocket.rs index 1d996a17..9c0260f0 100644 --- a/crates/core/result/src/rocket.rs +++ b/crates/core/result/src/rocket.rs @@ -90,6 +90,8 @@ impl<'r> Responder<'r, 'static> for Error { ErrorType::FileTypeNotAllowed => Status::BadRequest, ErrorType::ImageProcessingFailed => Status::InternalServerError, ErrorType::NoEmbedData => Status::BadRequest, + + ErrorType::ImATeaPot => Status::ImATeapot, }; // Serialize the error data structure into JSON. From f8eb324543668584be5be74166b50898305d9bd2 Mon Sep 17 00:00:00 2001 From: IAmTomahawkx Date: Fri, 20 Jun 2025 16:40:01 -0700 Subject: [PATCH 2/4] create models --- .../src/models/admin_audits/models.rs | 24 +- .../src/models/admin_case_comments/models.rs | 38 +++ .../database/src/models/admin_cases/models.rs | 32 +++ .../database/src/models/admin_notes/models.rs | 10 + .../src/models/admin_strikes/models.rs | 25 ++ .../database/src/models/admin_tokens/axum.rs | 32 ++- .../src/models/admin_tokens/models.rs | 18 +- .../src/models/admin_tokens/rocket.rs | 35 ++- .../database/src/models/admin_users/models.rs | 27 ++ .../database/src/models/admin_users/ops.rs | 2 + .../src/models/admin_users/ops/mongodb.rs | 4 + .../src/models/admin_users/ops/reference.rs | 9 + crates/core/database/src/util/basic.rs | 3 + crates/core/database/src/util/mod.rs | 1 + crates/core/models/src/v0/admin.rs | 261 ++++++++++++++++++ crates/core/models/src/v0/mod.rs | 2 + crates/core/models/src/v0/safety_reports.rs | 2 + crates/core/models/src/v0/users.rs | 2 + 18 files changed, 499 insertions(+), 28 deletions(-) create mode 100644 crates/core/database/src/util/basic.rs create mode 100644 crates/core/models/src/v0/admin.rs diff --git a/crates/core/database/src/models/admin_audits/models.rs b/crates/core/database/src/models/admin_audits/models.rs index 8a7b2d35..67f5045a 100644 --- a/crates/core/database/src/models/admin_audits/models.rs +++ b/crates/core/database/src/models/admin_audits/models.rs @@ -1,3 +1,5 @@ +use crate::util::basic::transform_optional_string; + auto_derived! { pub struct AdminAuditItem { /// The audit item ID @@ -10,8 +12,28 @@ auto_derived! { /// The relevant case ID, if applicable pub case_id: Option, /// The object the action was taken against, if applicable - pub target: Option, + pub target_id: Option, /// The context of the action, if applicable (eg. search phrases) pub context: Option, } } + +impl AdminAuditItem { + pub fn new( + mod_id: &str, + action: &str, + case_id: Option<&str>, + target_id: Option<&str>, + context: Option<&str>, + ) -> AdminAuditItem { + let id = ulid::Ulid::new().to_string(); + AdminAuditItem { + id, + mod_id: mod_id.to_string(), + action: action.to_string(), + case_id: transform_optional_string(case_id), + target_id: transform_optional_string(target_id), + context: transform_optional_string(context), + } + } +} diff --git a/crates/core/database/src/models/admin_case_comments/models.rs b/crates/core/database/src/models/admin_case_comments/models.rs index 8a54c2a7..0429d9ce 100644 --- a/crates/core/database/src/models/admin_case_comments/models.rs +++ b/crates/core/database/src/models/admin_case_comments/models.rs @@ -14,3 +14,41 @@ auto_derived_partial! { }, "PartialAdminCaseComment" } + +impl AdminCaseComment { + pub fn new(case_id: &str, user_id: &str, content: &str) -> AdminCaseComment { + let id = ulid::Ulid::new().to_string(); + AdminCaseComment { + id, + case_id: case_id.to_string(), + user_id: user_id.to_string(), + edited_at: None, + content: content.to_string(), + } + } + + /// Edit the comment, updating the edited_at time as well + pub fn edit(&mut self, content: &str) { + self.content = content.to_string(); + self.edited_at = Some( + iso8601_timestamp::Timestamp::now_utc() + .format_short() + .to_string(), + ); + } +} + +impl PartialAdminCaseComment { + pub fn new() -> PartialAdminCaseComment { + PartialAdminCaseComment::default() + } + /// Edit the comment, updating the edited_at time as well + pub fn edit(&mut self, content: &str) { + self.content = Some(content.to_string()); + self.edited_at = Some( + iso8601_timestamp::Timestamp::now_utc() + .format_short() + .to_string(), + ); + } +} diff --git a/crates/core/database/src/models/admin_cases/models.rs b/crates/core/database/src/models/admin_cases/models.rs index 0764b139..ac07a6ac 100644 --- a/crates/core/database/src/models/admin_cases/models.rs +++ b/crates/core/database/src/models/admin_cases/models.rs @@ -19,3 +19,35 @@ auto_derived_partial! { }, "PartialAdminCase" } + +impl AdminCase { + pub fn new(owner_id: &str, title: &str) -> AdminCase { + let id = ulid::Ulid::new().to_string(); + let short_id = id.clone().split_off(id.len() - 7); + + AdminCase { + id, + short_id, + owner_id: owner_id.to_string(), + title: title.to_string(), + status: "Open".to_string(), + closed_at: None, + tags: vec![], + } + } + + pub fn merge_tags(&self, other: &[String]) -> Vec { + let mut resp: Vec = vec![]; + // Shitty combining chain; itll only ever be like 5 items + resp.extend(self.tags.clone()); + resp.extend(other.iter().filter_map(|p| { + if !self.tags.contains(p) { + Some(p.clone()) + } else { + None + } + })); + + resp + } +} diff --git a/crates/core/database/src/models/admin_notes/models.rs b/crates/core/database/src/models/admin_notes/models.rs index ff3767c0..f4be8229 100644 --- a/crates/core/database/src/models/admin_notes/models.rs +++ b/crates/core/database/src/models/admin_notes/models.rs @@ -14,6 +14,16 @@ auto_derived_partial! { } impl AdminObjectNote { + pub fn new(object_id: &str, user_id: &str, content: &str) -> AdminObjectNote { + AdminObjectNote { + id: object_id.to_string(), + edited_at: iso8601_timestamp::Timestamp::now_utc() + .format_short() + .to_string(), + last_edited_by_id: user_id.to_string(), + content: content.to_string(), + } + } pub fn to_partial(&self) -> PartialAdminObjectNote { PartialAdminObjectNote { id: Some(self.id.clone()), diff --git a/crates/core/database/src/models/admin_strikes/models.rs b/crates/core/database/src/models/admin_strikes/models.rs index 3d8db55f..def17cdd 100644 --- a/crates/core/database/src/models/admin_strikes/models.rs +++ b/crates/core/database/src/models/admin_strikes/models.rs @@ -1,3 +1,5 @@ +use crate::util::basic::transform_optional_string; + auto_derived_partial! { pub struct AdminStrike { /// The strike ID @@ -20,3 +22,26 @@ auto_derived_partial! { }, "PartialAdminStrike" } + +impl AdminStrike { + pub fn new( + target_id: &str, + mod_id: &str, + case_id: Option<&str>, + associated_action: Option<&str>, + reason: &str, + mod_context: Option<&str>, + ) -> AdminStrike { + let id = ulid::Ulid::new().to_string(); + AdminStrike { + id, + target_id: target_id.to_string(), + mod_id: mod_id.to_string(), + case_id: transform_optional_string(case_id), + associated_action: transform_optional_string(associated_action), + overruled: false, + reason: reason.to_string(), + mod_context: transform_optional_string(mod_context), + } + } +} diff --git a/crates/core/database/src/models/admin_tokens/axum.rs b/crates/core/database/src/models/admin_tokens/axum.rs index cf98fb68..1233ae8f 100644 --- a/crates/core/database/src/models/admin_tokens/axum.rs +++ b/crates/core/database/src/models/admin_tokens/axum.rs @@ -8,17 +8,29 @@ use crate::{AdminMachineToken, Database}; impl FromRequestParts for AdminMachineToken { type Rejection = Error; - async fn from_request_parts(parts: &mut Parts, _db: &Database) -> Result { - if let Some(Ok(token)) = parts.headers.get("x-admin-machine").map(|v| v.to_str()) { - let config = revolt_config::config().await; - let token = token.to_string(); - if config.api.security.admin_keys.contains(&token) { - Ok(AdminMachineToken::new()) - } else { - Err(create_error!(InvalidCredentials)) + async fn from_request_parts(parts: &mut Parts, db: &Database) -> Result { + if let Some(Ok(on_behalf_of)) = parts + .headers + .get("x-admin-on-behalf-of") + .map(|v| v.to_str()) + { + if let Some(Ok(token)) = parts.headers.get("x-admin-machine").map(|v| v.to_str()) { + let config = revolt_config::config().await; + let token = token.to_string(); + if config.api.security.admin_keys.contains(&token) { + let resp: AdminMachineToken; + // shitty email check + if on_behalf_of.contains("@") { + resp = AdminMachineToken::new_from_email(on_behalf_of, db).await? + } else { + resp = AdminMachineToken::new_from_id(on_behalf_of, db).await? + } + return Ok(resp); + } else { + return Err(create_error!(InvalidCredentials)); + } } - } else { - Err(create_error!(NotAuthenticated)) } + Err(create_error!(NotAuthenticated)) } } diff --git a/crates/core/database/src/models/admin_tokens/models.rs b/crates/core/database/src/models/admin_tokens/models.rs index ce4c4a7a..0ffe3dd1 100644 --- a/crates/core/database/src/models/admin_tokens/models.rs +++ b/crates/core/database/src/models/admin_tokens/models.rs @@ -1,3 +1,7 @@ +use revolt_result::Result; + +use crate::{AdminUser, Database}; + auto_derived! { pub struct AdminToken { /// The token ID @@ -14,18 +18,18 @@ auto_derived! { /// This struct is used to validate machine tokens when doing machine to machine communication. pub struct AdminMachineToken { /// Placeholder field. - pub valid: bool + pub on_behalf_of: AdminUser } } impl AdminMachineToken { - pub fn new() -> AdminMachineToken { - AdminMachineToken { valid: true } + pub async fn new_from_email(email: &str, db: &Database) -> Result { + let user = db.admin_user_fetch_email(email).await?; + Ok(AdminMachineToken { on_behalf_of: user }) } -} -impl Default for AdminMachineToken { - fn default() -> Self { - AdminMachineToken::new() + pub async fn new_from_id(user_id: &str, db: &Database) -> Result { + let user = db.admin_user_fetch(user_id).await?; + Ok(AdminMachineToken { on_behalf_of: user }) } } diff --git a/crates/core/database/src/models/admin_tokens/rocket.rs b/crates/core/database/src/models/admin_tokens/rocket.rs index 15e6078e..ed5b771b 100644 --- a/crates/core/database/src/models/admin_tokens/rocket.rs +++ b/crates/core/database/src/models/admin_tokens/rocket.rs @@ -1,7 +1,8 @@ +use revolt_result::Result; use rocket::http::Status; use rocket::request::{self, FromRequest, Outcome, Request}; -use crate::AdminMachineToken; +use crate::{AdminMachineToken, Database}; #[rocket::async_trait] impl<'r> FromRequest<'r> for AdminMachineToken { @@ -10,20 +11,34 @@ impl<'r> FromRequest<'r> for AdminMachineToken { async fn from_request(request: &'r Request<'_>) -> request::Outcome { let user: &Option = request .local_cache_async(async { - let config = revolt_config::config().await; + let db = request.rocket().state::().expect("`Database`"); - let token = request + if let Some(on_behalf_of) = request .headers() - .get("x-admin-machine") + .get("x-admin-on-behalf-of") .next() - .map(|x| x.to_string()); - - if let Some(token) = token { - if config.api.security.admin_keys.contains(&token) { - return Some(AdminMachineToken::new()); + .map(|x| x.to_string()) + { + if let Some(token) = request + .headers() + .get("x-admin-machine") + .next() + .map(|x| x.to_string()) + { + let config = revolt_config::config().await; + let token = token.to_string(); + if config.api.security.admin_keys.contains(&token) { + let resp: Result = if on_behalf_of.contains("@") { + AdminMachineToken::new_from_email(&on_behalf_of, db).await + } else { + AdminMachineToken::new_from_id(&on_behalf_of, db).await + }; + if let Ok(resp) = resp { + return Some(resp); + } + } } } - None }) .await; diff --git a/crates/core/database/src/models/admin_users/models.rs b/crates/core/database/src/models/admin_users/models.rs index 2242fe72..a931331b 100644 --- a/crates/core/database/src/models/admin_users/models.rs +++ b/crates/core/database/src/models/admin_users/models.rs @@ -1,8 +1,14 @@ +use revolt_result::Result; + +use crate::Database; + auto_derived_partial! { pub struct AdminUser { /// The ID of the user #[serde(rename = "_id")] pub id: String, + /// The user's revolt ID. + pub platform_user_id: String, /// The user's email pub email: String, /// Whether the user is active or not (ie. can they use the api) @@ -12,3 +18,24 @@ auto_derived_partial! { }, "PartialAdminUser" } + +impl AdminUser { + pub fn new(email: &str, platform_user_id: &str, permissions: u64) -> AdminUser { + let id = ulid::Ulid::new().to_string(); + AdminUser { + id, + platform_user_id: platform_user_id.to_string(), + email: email.to_string(), + active: true, + permissions, + } + } + + pub async fn find_by_id(id: &str, db: &Database) -> Result { + return db.admin_user_fetch(id).await; + } + + pub async fn find_by_email(email: &str, db: &Database) -> Result { + return db.admin_user_fetch_email(email).await; + } +} diff --git a/crates/core/database/src/models/admin_users/ops.rs b/crates/core/database/src/models/admin_users/ops.rs index 664aca4a..044f380d 100644 --- a/crates/core/database/src/models/admin_users/ops.rs +++ b/crates/core/database/src/models/admin_users/ops.rs @@ -12,5 +12,7 @@ pub trait AbstractAdminUsers: Sync + Send { async fn admin_user_fetch(&self, user_id: &str) -> Result; + async fn admin_user_fetch_email(&self, email: &str) -> Result; + async fn admin_user_list(&self) -> Result>; } diff --git a/crates/core/database/src/models/admin_users/ops/mongodb.rs b/crates/core/database/src/models/admin_users/ops/mongodb.rs index 11513db5..2f6fd552 100644 --- a/crates/core/database/src/models/admin_users/ops/mongodb.rs +++ b/crates/core/database/src/models/admin_users/ops/mongodb.rs @@ -21,6 +21,10 @@ impl AbstractAdminUsers for MongoDb { query!(self, find_one_by_id, COL, user_id)?.ok_or_else(|| create_error!(NotFound)) } + async fn admin_user_fetch_email(&self, email: &str) -> Result { + query!(self, find_one, COL, doc! {"email": email})?.ok_or_else(|| create_error!(NotFound)) + } + async fn admin_user_list(&self) -> Result> { query!(self, find, COL, doc! {}) } diff --git a/crates/core/database/src/models/admin_users/ops/reference.rs b/crates/core/database/src/models/admin_users/ops/reference.rs index 5bee2e81..dd75cdb4 100644 --- a/crates/core/database/src/models/admin_users/ops/reference.rs +++ b/crates/core/database/src/models/admin_users/ops/reference.rs @@ -36,6 +36,15 @@ impl AbstractAdminUsers for ReferenceDb { } } + async fn admin_user_fetch_email(&self, email: &str) -> Result { + let admin_users = self.admin_users.lock().await; + if let Some((_, user)) = admin_users.iter().filter(|(_, p)| p.email == email).next() { + Ok(user.clone()) + } else { + Err(create_error!(NotFound)) + } + } + async fn admin_user_list(&self) -> Result> { let admin_users = self.admin_users.lock().await; Ok(admin_users.iter().map(|(_, u)| u).cloned().collect()) diff --git a/crates/core/database/src/util/basic.rs b/crates/core/database/src/util/basic.rs new file mode 100644 index 00000000..07165d2f --- /dev/null +++ b/crates/core/database/src/util/basic.rs @@ -0,0 +1,3 @@ +pub fn transform_optional_string(s: Option<&str>) -> Option { + s.map(|f| f.to_string()) +} diff --git a/crates/core/database/src/util/mod.rs b/crates/core/database/src/util/mod.rs index 1baf7d8b..065151aa 100644 --- a/crates/core/database/src/util/mod.rs +++ b/crates/core/database/src/util/mod.rs @@ -1,3 +1,4 @@ +pub mod basic; pub mod bridge; pub mod bulk_permissions; pub mod idempotency; diff --git a/crates/core/models/src/v0/admin.rs b/crates/core/models/src/v0/admin.rs new file mode 100644 index 00000000..2dd0668c --- /dev/null +++ b/crates/core/models/src/v0/admin.rs @@ -0,0 +1,261 @@ +use iso8601_timestamp::Timestamp; + +use crate::v0::Report; + +auto_derived! { + #[derive(Default)] + #[cfg_attr(feature = "validator", derive(validator::Validate))] + pub struct AdminAuditItemCreate { + /// The ID of the mod who performed the action + #[cfg_attr(feature="serde", serde(rename="mod"))] + pub mod_id: String, + /// The action performed (previously 'permission') + pub action: String, + /// The relevant case ID, if applicable + #[cfg_attr(feature="serde", serde(rename="case"))] + pub case_id: String, + /// The id of the target (any object) the action was taken against, if applicable + #[cfg_attr(feature="serde", serde(rename="target"))] + pub target_id: Option, + /// The context attached to the action, if applicable (eg. search phrases) + pub context: Option, + } + + pub struct AdminAuditItem { + /// The audit item ID + pub id: String, + /// The ID of the mod who performed the action + #[cfg_attr(feature="serde", serde(rename="mod"))] + pub mod_id: String, + /// The action performed (previously 'permission') + pub action: String, + /// The relevant case ID, if applicable + #[cfg_attr(feature="serde", serde(rename="case"))] + pub case_id: String, + /// The id of the target (any object) the action was taken against, if applicable + #[cfg_attr(feature="serde", serde(rename="target"))] + pub target_id: Option, + /// The context attached to the action, if applicable (eg. search phrases) + pub context: Option, + } + + #[derive(Default)] + #[cfg_attr(feature = "validator", derive(validator::Validate))] + pub struct AdminCaseCommentCreate { + /// The ID of the case this comment is attached to + #[cfg_attr(feature="serde", serde(rename="case"))] + pub case_id: String, + /// The content + #[cfg_attr(feature = "validator", validate(length(min = 1, max = 2000)))] + pub content: String + } + + #[derive(Default)] + #[cfg_attr(feature = "validator", derive(validator::Validate))] + pub struct AdminCaseCommentEdit { + /// The new content + #[cfg_attr(feature = "validator", validate(length(min = 1, max = 2000)))] + pub content: String + } + + pub struct AdminCaseComment { + /// The comment ID + pub id: String, + /// The ID of the case this comment is attached to + #[cfg_attr(feature="serde", serde(rename="case"))] + pub case_id: String, + /// The user who posted the comment + #[cfg_attr(feature="serde", serde(rename="user"))] + pub user_id: String, + /// When the comment was edited, if applicable, in iso8601 + pub edited_at: Option, + /// The content + pub content: String + } + + #[derive(Default)] + #[cfg_attr(feature = "validator", derive(validator::Validate))] + pub struct AdminCaseCreate { + /// The owner of the case. Defaults to the creator + #[cfg_attr(feature="serde", serde(rename="owner"))] + pub owner: Option, + /// The title of the case. If not provided, a default will be generated from the report(s) assigned + pub title: Option, + /// The report IDs to initially attach to this case (and generate a default title from) + #[cfg_attr(feature = "validator", validate(length(min = 1)))] + pub initial_reports: Vec + } + + #[derive(Default)] + pub struct AdminCaseEdit { + /// The new owner of the case. + #[cfg_attr(feature="serde", serde(rename="owner"))] + pub owner_id: Option, + /// The new title of the case. + pub title: Option, + /// Report IDs to add to the case. + pub add_reports: Option>, + /// Report IDs to remove from the case. + pub remove_reports: Option> + } + + pub struct AdminCase { + /// The case ID + pub id: String, + /// The case Short ID + pub short_id: String, + /// The owner of the case + #[cfg_attr(feature="serde", serde(rename="owner"))] + pub owner_id: String, + /// The title of the case + pub title: String, + /// The status of the case (open/closed) + pub status: String, + /// When the case was closed + pub closed_at: Option, + /// The tags for the case + pub tags: Vec, + /// The reports assigned to this case + pub reports: Vec + } + + + pub struct AdminObjectNote { + /// When the note was edited, in iso8601 + pub edited_at: Timestamp, + /// The last user to edit the note + #[cfg_attr(feature="serde", serde(rename="last_edited_by"))] + pub last_edited_by_id: String, + /// The content of the note + pub content: String, + } + + #[cfg_attr(feature = "validator", derive(validator::Validate))] + pub struct AdminObjectNoteEdit { + #[cfg_attr(feature = "validator", validate(length(max = 2000)))] + pub content: String + } + + pub struct AdminStrike { + /// The strike ID + pub id: String, + /// The object receiving the strike (user/server) + #[cfg_attr(feature="serde", serde(rename="target"))] + pub target_id: String, + /// The moderator who gave the strike + #[cfg_attr(feature="serde", serde(rename="mod"))] + pub mod_id: String, + /// The case the strike was made under + #[cfg_attr(feature="serde", serde(rename="case"))] + #[cfg_attr(feature="serde", serde(skip_serializing_if="Option::is_none"))] + pub case_id: Option, + /// Action associated with the strike (eg. suspension/ban) + #[cfg_attr(feature="serde", serde(skip_serializing_if="Option::is_none"))] + pub associated_action: Option, + /// Has the strike been removed + #[cfg_attr(feature="serde", serde(skip_serializing_if="crate::if_false"))] + pub overruled: bool, + /// The user-facing reason for the strike + pub reason: String, + /// Internal context for the strike + #[cfg_attr(feature="serde", serde(skip_serializing_if="Option::is_none"))] + pub mod_context: Option, + } + + #[cfg_attr(feature = "validator", derive(validator::Validate))] + pub struct AdminStrikeCreate { + /// The object receiving the strike (user/server) + #[cfg_attr(feature="serde", serde(rename="target"))] + pub target_id: String, + /// The case the strike was made under + #[cfg_attr(feature="serde", serde(rename="case"))] + pub case_id: Option, + /// Action associated with the strike (eg. suspension/ban) + #[cfg_attr(feature = "validator", validate(length(min = 1, max = 25)))] + pub associated_action: Option, + /// The user-facing reason for the strike + #[cfg_attr(feature = "validator", validate(length(max = 2000)))] + pub reason: String, + /// Internal context for the strike + #[cfg_attr(feature = "validator", validate(length(max = 2000)))] + pub mod_context: Option, + } + + #[cfg_attr(feature = "validator", derive(validator::Validate))] + pub struct AdminStrikeEdit { + /// The case the strike was made under + #[cfg_attr(feature="serde", serde(rename="case"))] + pub case_id: Option, + /// Action associated with the strike (eg. suspension/ban) + #[cfg_attr(feature = "validator", validate(length(min = 1, max = 25)))] + pub associated_action: Option, + /// The user-facing reason for the strike + #[cfg_attr(feature = "validator", validate(length(max = 2000)))] + pub reason: Option, + /// Internal context for the strike + #[cfg_attr(feature = "validator", validate(length(max = 2000)))] + pub mod_context: Option, + } + + pub struct AdminToken { + /// The token ID + pub id: String, + /// The user this token is attached to + #[cfg_attr(feature = "serde", serde(rename="user"))] + pub user_id: String, + /// The token itself + pub token: String, + /// The expiry timestamp for this token, in iso6801 + pub expiry: Timestamp + } + + #[cfg_attr(feature = "validator", derive(validator::Validate))] + pub struct AdminTokenCreate { + /// the user who this token is for + #[cfg_attr(feature = "serde", serde(rename="user"))] + pub user_id: String, + /// The expiry timestamp for this token, in iso6801. Max 30 days from current time. + pub expiry: Timestamp, + } + + pub struct AdminUser { + /// The ID of the user + pub id: String, + /// The user's revolt ID. + pub platform_user_id: String, + /// The user's email + pub email: String, + /// Whether the user is active or not (ie. can they use the api) + pub active: bool, + /// The permissions of the user + pub permissions: u64, + } + + #[cfg_attr(feature = "validator", derive(validator::Validate))] + pub struct AdminUserCreate { + /// The user's revolt ID. + #[cfg_attr(feature = "validator", validate(length(min = 10, max = 20)))] + pub platform_user_id: String, + /// The user's email + #[cfg_attr(feature = "validator", validate(email))] + pub email: String, + /// Whether the user is active or not (ie. can they use the api) + pub active: bool, + /// The permissions of the user + pub permissions: u64, + } + + #[cfg_attr(feature = "validator", derive(validator::Validate))] + pub struct AdminUserEdit { + /// The user's revolt ID. + #[cfg_attr(feature = "validator", validate(length(min = 10, max = 20)))] + pub platform_user_id: Option, + /// The user's email + #[cfg_attr(feature = "validator", validate(email))] + pub email: Option, + /// Whether the user is active or not (ie. can they use the api) + pub active: Option, + /// The permissions of the user + pub permissions: Option, + } +} diff --git a/crates/core/models/src/v0/mod.rs b/crates/core/models/src/v0/mod.rs index 0468493b..5a81a23e 100644 --- a/crates/core/models/src/v0/mod.rs +++ b/crates/core/models/src/v0/mod.rs @@ -1,3 +1,4 @@ +mod admin; mod bots; mod channel_invites; mod channel_unreads; @@ -15,6 +16,7 @@ mod servers; mod user_settings; mod users; +pub use admin::*; pub use bots::*; pub use channel_invites::*; pub use channel_unreads::*; diff --git a/crates/core/models/src/v0/safety_reports.rs b/crates/core/models/src/v0/safety_reports.rs index 0d8ee5d7..07579425 100644 --- a/crates/core/models/src/v0/safety_reports.rs +++ b/crates/core/models/src/v0/safety_reports.rs @@ -8,6 +8,8 @@ auto_derived!( pub id: String, /// Id of the user creating this report pub author_id: String, + /// The case this report is attached to + pub case_id: Option, /// Reported content pub content: ReportedContent, /// Additional report context diff --git a/crates/core/models/src/v0/users.rs b/crates/core/models/src/v0/users.rs index ba913dd0..f8aa0b79 100644 --- a/crates/core/models/src/v0/users.rs +++ b/crates/core/models/src/v0/users.rs @@ -200,6 +200,8 @@ auto_derived!( Banned = 4, /// User was marked as spam and removed from platform Spam = 8, + /// User is a global moderator + GlobalModerator = 16, } /// New user profile data From ff36d71ad551999ae06011ee40d80a7e74139428 Mon Sep 17 00:00:00 2001 From: IAmTomahawkx Date: Wed, 2 Jul 2025 18:43:23 -0700 Subject: [PATCH 3/4] feat: Implement models and meta routes. Add empty files for routes still to be implemented. --- crates/core/config/Revolt.toml | 2 + crates/core/config/src/lib.rs | 1 + .../database/src/models/admin_tokens/axum.rs | 36 ---- .../database/src/models/admin_tokens/mod.rs | 7 +- .../src/models/admin_tokens/models.rs | 36 ++++ .../database/src/models/admin_tokens/ops.rs | 2 + .../src/models/admin_tokens/ops/mongodb.rs | 4 + .../src/models/admin_tokens/ops/reference.rs | 9 + .../src/models/admin_tokens/rocket.rs | 96 ++++++++++- .../src/models/admin_tokens/schema.rs | 59 +++++++ .../database/src/models/admin_users/axum.rs | 19 -- .../database/src/models/admin_users/mod.rs | 4 - .../database/src/models/admin_users/models.rs | 28 +++ .../database/src/models/admin_users/ops.rs | 2 + .../src/models/admin_users/ops/mongodb.rs | 4 + .../src/models/admin_users/ops/reference.rs | 4 + .../database/src/models/admin_users/rocket.rs | 8 +- crates/core/database/src/util/bridge/v0.rs | 70 ++++++++ crates/core/database/src/util/reference.rs | 8 +- crates/core/models/src/v0/admin.rs | 35 +++- .../routes/admin/accounts/account_delete.rs | 0 .../routes/admin/accounts/account_disable.rs | 0 .../admin/accounts/account_email_change.rs | 0 .../admin/accounts/account_email_verify.rs | 0 .../routes/admin/accounts/account_enable.rs | 0 .../admin/accounts/account_mfa_reset.rs | 1 + .../admin/accounts/account_reset_lockout.rs | 0 crates/delta/src/routes/admin/accounts/mod.rs | 1 + .../admin/cases/actions/case_add_comment.rs | 1 + .../routes/admin/cases/actions/case_create.rs | 0 .../admin/cases/actions/case_edit_tags.rs | 0 .../routes/admin/cases/actions/case_rename.rs | 0 .../cases/actions/case_send_notification.rs | 0 .../routes/admin/cases/actions/case_state.rs | 1 + .../src/routes/admin/cases/actions/mod.rs | 0 .../src/routes/admin/cases/fetch/case_get.rs | 1 + .../admin/cases/fetch/case_get_all_open.rs | 0 .../delta/src/routes/admin/cases/fetch/mod.rs | 0 crates/delta/src/routes/admin/cases/mod.rs | 2 + .../admin/channels/actions/channel_delete.rs | 0 .../admin/channels/actions/channel_edit.rs | 0 .../admin/channels/actions/channel_wipe.rs | 0 .../src/routes/admin/channels/actions/mod.rs | 0 .../admin/channels/fetch/channel_get.rs | 0 .../fetch/channel_get_participants.rs | 0 .../src/routes/admin/channels/fetch/mod.rs | 0 crates/delta/src/routes/admin/channels/mod.rs | 2 + .../delta/src/routes/admin/messages/delete.rs | 0 .../src/routes/admin/meta/create_token.rs | 38 ++++ .../src/routes/admin/meta/create_user.rs | 26 +++ .../delta/src/routes/admin/meta/edit_user.rs | 27 +++ .../src/routes/admin/meta/fetch_users.rs | 29 ++++ crates/delta/src/routes/admin/meta/mod.rs | 5 + .../src/routes/admin/meta/revoke_token.rs | 39 +++++ crates/delta/src/routes/admin/mod.rs | 26 +++ .../src/routes/admin/object_edit_note.rs | 28 +++ .../delta/src/routes/admin/object_get_note.rs | 24 +++ .../src/routes/admin/reports/actions/mod.rs | 0 .../admin/reports/actions/report_assign.rs | 0 .../admin/reports/actions/report_create.rs | 0 .../admin/reports/actions/report_edit.rs | 0 .../reports/actions/reports_bulk_assign.rs | 0 .../src/routes/admin/reports/fetch/mod.rs | 0 .../admin/reports/fetch/reports_unassigned.rs | 0 crates/delta/src/routes/admin/reports/mod.rs | 2 + crates/delta/src/routes/admin/roles/mod.rs | 3 + .../src/routes/admin/roles/role_create.rs | 0 .../src/routes/admin/roles/role_delete.rs | 0 .../delta/src/routes/admin/roles/role_edit.rs | 0 crates/delta/src/routes/admin/search/mod.rs | 1 + .../routes/admin/search/search_everything.rs | 0 .../src/routes/admin/servers/actions/mod.rs | 0 .../servers/actions/server_add_members.rs | 0 .../servers/actions/server_change_owner.rs | 0 .../servers/actions/server_create_invite.rs | 0 .../admin/servers/actions/server_delete.rs | 0 .../servers/actions/server_delete_invites.rs | 0 .../admin/servers/actions/server_edit.rs | 0 .../servers/actions/server_remove_members.rs | 0 .../admin/servers/actions/server_set_flags.rs | 0 .../src/routes/admin/servers/fetch/mod.rs | 0 .../routes/admin/servers/fetch/server_get.rs | 0 .../servers/fetch/server_get_participants.rs | 0 crates/delta/src/routes/admin/servers/mod.rs | 2 + .../src/routes/admin/users/actions/mod.rs | 0 .../routes/admin/users/actions/user_ban.rs | 0 .../admin/users/actions/user_data_package.rs | 0 .../admin/users/actions/user_reset_profile.rs | 0 .../admin/users/actions/user_send_alert.rs | 0 .../routes/admin/users/actions/user_strike.rs | 0 .../admin/users/actions/user_suspend.rs | 0 .../routes/admin/users/actions/user_unban.rs | 0 .../admin/users/actions/user_unsuspend.rs | 0 .../admin/users/actions/user_wipe_messages.rs | 0 .../delta/src/routes/admin/users/fetch/mod.rs | 0 .../src/routes/admin/users/fetch/user_get.rs | 0 crates/delta/src/routes/admin/users/mod.rs | 2 + crates/delta/src/routes/admin/util.rs | 15 ++ crates/delta/src/routes/mod.rs | 163 +++++++++--------- .../delta/src/routes/safety/report_content.rs | 1 + 100 files changed, 718 insertions(+), 156 deletions(-) delete mode 100644 crates/core/database/src/models/admin_tokens/axum.rs create mode 100644 crates/core/database/src/models/admin_tokens/schema.rs delete mode 100644 crates/core/database/src/models/admin_users/axum.rs create mode 100644 crates/delta/src/routes/admin/accounts/account_delete.rs create mode 100644 crates/delta/src/routes/admin/accounts/account_disable.rs create mode 100644 crates/delta/src/routes/admin/accounts/account_email_change.rs create mode 100644 crates/delta/src/routes/admin/accounts/account_email_verify.rs create mode 100644 crates/delta/src/routes/admin/accounts/account_enable.rs create mode 100644 crates/delta/src/routes/admin/accounts/account_mfa_reset.rs create mode 100644 crates/delta/src/routes/admin/accounts/account_reset_lockout.rs create mode 100644 crates/delta/src/routes/admin/accounts/mod.rs create mode 100644 crates/delta/src/routes/admin/cases/actions/case_add_comment.rs create mode 100644 crates/delta/src/routes/admin/cases/actions/case_create.rs create mode 100644 crates/delta/src/routes/admin/cases/actions/case_edit_tags.rs create mode 100644 crates/delta/src/routes/admin/cases/actions/case_rename.rs create mode 100644 crates/delta/src/routes/admin/cases/actions/case_send_notification.rs create mode 100644 crates/delta/src/routes/admin/cases/actions/case_state.rs create mode 100644 crates/delta/src/routes/admin/cases/actions/mod.rs create mode 100644 crates/delta/src/routes/admin/cases/fetch/case_get.rs create mode 100644 crates/delta/src/routes/admin/cases/fetch/case_get_all_open.rs create mode 100644 crates/delta/src/routes/admin/cases/fetch/mod.rs create mode 100644 crates/delta/src/routes/admin/cases/mod.rs create mode 100644 crates/delta/src/routes/admin/channels/actions/channel_delete.rs create mode 100644 crates/delta/src/routes/admin/channels/actions/channel_edit.rs create mode 100644 crates/delta/src/routes/admin/channels/actions/channel_wipe.rs create mode 100644 crates/delta/src/routes/admin/channels/actions/mod.rs create mode 100644 crates/delta/src/routes/admin/channels/fetch/channel_get.rs create mode 100644 crates/delta/src/routes/admin/channels/fetch/channel_get_participants.rs create mode 100644 crates/delta/src/routes/admin/channels/fetch/mod.rs create mode 100644 crates/delta/src/routes/admin/channels/mod.rs create mode 100644 crates/delta/src/routes/admin/messages/delete.rs create mode 100644 crates/delta/src/routes/admin/meta/create_token.rs create mode 100644 crates/delta/src/routes/admin/meta/create_user.rs create mode 100644 crates/delta/src/routes/admin/meta/edit_user.rs create mode 100644 crates/delta/src/routes/admin/meta/fetch_users.rs create mode 100644 crates/delta/src/routes/admin/meta/mod.rs create mode 100644 crates/delta/src/routes/admin/meta/revoke_token.rs create mode 100644 crates/delta/src/routes/admin/mod.rs create mode 100644 crates/delta/src/routes/admin/object_edit_note.rs create mode 100644 crates/delta/src/routes/admin/object_get_note.rs create mode 100644 crates/delta/src/routes/admin/reports/actions/mod.rs create mode 100644 crates/delta/src/routes/admin/reports/actions/report_assign.rs create mode 100644 crates/delta/src/routes/admin/reports/actions/report_create.rs create mode 100644 crates/delta/src/routes/admin/reports/actions/report_edit.rs create mode 100644 crates/delta/src/routes/admin/reports/actions/reports_bulk_assign.rs create mode 100644 crates/delta/src/routes/admin/reports/fetch/mod.rs create mode 100644 crates/delta/src/routes/admin/reports/fetch/reports_unassigned.rs create mode 100644 crates/delta/src/routes/admin/reports/mod.rs create mode 100644 crates/delta/src/routes/admin/roles/mod.rs create mode 100644 crates/delta/src/routes/admin/roles/role_create.rs create mode 100644 crates/delta/src/routes/admin/roles/role_delete.rs create mode 100644 crates/delta/src/routes/admin/roles/role_edit.rs create mode 100644 crates/delta/src/routes/admin/search/mod.rs create mode 100644 crates/delta/src/routes/admin/search/search_everything.rs create mode 100644 crates/delta/src/routes/admin/servers/actions/mod.rs create mode 100644 crates/delta/src/routes/admin/servers/actions/server_add_members.rs create mode 100644 crates/delta/src/routes/admin/servers/actions/server_change_owner.rs create mode 100644 crates/delta/src/routes/admin/servers/actions/server_create_invite.rs create mode 100644 crates/delta/src/routes/admin/servers/actions/server_delete.rs create mode 100644 crates/delta/src/routes/admin/servers/actions/server_delete_invites.rs create mode 100644 crates/delta/src/routes/admin/servers/actions/server_edit.rs create mode 100644 crates/delta/src/routes/admin/servers/actions/server_remove_members.rs create mode 100644 crates/delta/src/routes/admin/servers/actions/server_set_flags.rs create mode 100644 crates/delta/src/routes/admin/servers/fetch/mod.rs create mode 100644 crates/delta/src/routes/admin/servers/fetch/server_get.rs create mode 100644 crates/delta/src/routes/admin/servers/fetch/server_get_participants.rs create mode 100644 crates/delta/src/routes/admin/servers/mod.rs create mode 100644 crates/delta/src/routes/admin/users/actions/mod.rs create mode 100644 crates/delta/src/routes/admin/users/actions/user_ban.rs create mode 100644 crates/delta/src/routes/admin/users/actions/user_data_package.rs create mode 100644 crates/delta/src/routes/admin/users/actions/user_reset_profile.rs create mode 100644 crates/delta/src/routes/admin/users/actions/user_send_alert.rs create mode 100644 crates/delta/src/routes/admin/users/actions/user_strike.rs create mode 100644 crates/delta/src/routes/admin/users/actions/user_suspend.rs create mode 100644 crates/delta/src/routes/admin/users/actions/user_unban.rs create mode 100644 crates/delta/src/routes/admin/users/actions/user_unsuspend.rs create mode 100644 crates/delta/src/routes/admin/users/actions/user_wipe_messages.rs create mode 100644 crates/delta/src/routes/admin/users/fetch/mod.rs create mode 100644 crates/delta/src/routes/admin/users/fetch/user_get.rs create mode 100644 crates/delta/src/routes/admin/users/mod.rs create mode 100644 crates/delta/src/routes/admin/util.rs diff --git a/crates/core/config/Revolt.toml b/crates/core/config/Revolt.toml index fa9937bb..77642703 100644 --- a/crates/core/config/Revolt.toml +++ b/crates/core/config/Revolt.toml @@ -195,6 +195,8 @@ admin_api_enabled = false mass_mentions_send_notifications = true # Can role/everyone pings be used at all mass_mentions_enabled = true +# Show the admin api in the OpenAPI spec +admin_api_show_spec = false [features.limits] diff --git a/crates/core/config/src/lib.rs b/crates/core/config/src/lib.rs index ebf480b6..699c7d1f 100644 --- a/crates/core/config/src/lib.rs +++ b/crates/core/config/src/lib.rs @@ -335,6 +335,7 @@ pub struct Features { pub mass_mentions_send_notifications: bool, pub mass_mentions_enabled: bool, pub admin_api_enabled: bool, + pub admin_api_show_spec: bool, #[serde(default)] pub advanced: FeaturesAdvanced, diff --git a/crates/core/database/src/models/admin_tokens/axum.rs b/crates/core/database/src/models/admin_tokens/axum.rs deleted file mode 100644 index 1233ae8f..00000000 --- a/crates/core/database/src/models/admin_tokens/axum.rs +++ /dev/null @@ -1,36 +0,0 @@ -use axum::{extract::FromRequestParts, http::request::Parts}; - -use revolt_result::{create_error, Error, Result}; - -use crate::{AdminMachineToken, Database}; - -#[async_trait::async_trait] -impl FromRequestParts for AdminMachineToken { - type Rejection = Error; - - async fn from_request_parts(parts: &mut Parts, db: &Database) -> Result { - if let Some(Ok(on_behalf_of)) = parts - .headers - .get("x-admin-on-behalf-of") - .map(|v| v.to_str()) - { - if let Some(Ok(token)) = parts.headers.get("x-admin-machine").map(|v| v.to_str()) { - let config = revolt_config::config().await; - let token = token.to_string(); - if config.api.security.admin_keys.contains(&token) { - let resp: AdminMachineToken; - // shitty email check - if on_behalf_of.contains("@") { - resp = AdminMachineToken::new_from_email(on_behalf_of, db).await? - } else { - resp = AdminMachineToken::new_from_id(on_behalf_of, db).await? - } - return Ok(resp); - } else { - return Err(create_error!(InvalidCredentials)); - } - } - } - Err(create_error!(NotAuthenticated)) - } -} diff --git a/crates/core/database/src/models/admin_tokens/mod.rs b/crates/core/database/src/models/admin_tokens/mod.rs index 7b1a1988..8a22f64e 100644 --- a/crates/core/database/src/models/admin_tokens/mod.rs +++ b/crates/core/database/src/models/admin_tokens/mod.rs @@ -1,13 +1,12 @@ -#[cfg(feature = "axum-impl")] -mod axum; mod models; mod ops; #[cfg(feature = "rocket-impl")] mod rocket; +mod schema; -#[cfg(feature = "axum-impl")] -pub use self::axum::*; #[cfg(feature = "rocket-impl")] pub use self::rocket::*; +#[cfg(feature = "rocket-impl")] +pub use self::schema::*; pub use models::*; pub use ops::*; diff --git a/crates/core/database/src/models/admin_tokens/models.rs b/crates/core/database/src/models/admin_tokens/models.rs index 0ffe3dd1..bb87e142 100644 --- a/crates/core/database/src/models/admin_tokens/models.rs +++ b/crates/core/database/src/models/admin_tokens/models.rs @@ -1,3 +1,4 @@ +use iso8601_timestamp::Timestamp; use revolt_result::Result; use crate::{AdminUser, Database}; @@ -20,10 +21,45 @@ auto_derived! { /// Placeholder field. pub on_behalf_of: AdminUser } + + pub enum AdminAuthorization { + AdminUser(AdminUser), + AdminMachine(AdminMachineToken), + } +} + +impl AdminToken { + pub fn new(user_id: &str, expiry: Timestamp) -> AdminToken { + let id = ulid::Ulid::new().to_string(); + let token = nanoid::nanoid!(64); + AdminToken { + id, + user_id: user_id.to_string(), + token, + expiry: expiry.format_short().to_string(), + } + } } impl AdminMachineToken { pub async fn new_from_email(email: &str, db: &Database) -> Result { + println!("{}", email); + if email == "example@example.com" { + // This is basically just a workaround to make the first user. + let user_count = db.admin_user_count().await?; + println!("{}", user_count); + if user_count == 0 { + return Ok(AdminMachineToken { + on_behalf_of: AdminUser { + id: "00".to_string(), + platform_user_id: "".to_string(), + email: "example@example.com".to_string(), + active: true, + permissions: u64::MAX, + }, + }); + } + } let user = db.admin_user_fetch_email(email).await?; Ok(AdminMachineToken { on_behalf_of: user }) } diff --git a/crates/core/database/src/models/admin_tokens/ops.rs b/crates/core/database/src/models/admin_tokens/ops.rs index a42e73a3..f4d0f0e7 100644 --- a/crates/core/database/src/models/admin_tokens/ops.rs +++ b/crates/core/database/src/models/admin_tokens/ops.rs @@ -11,4 +11,6 @@ pub trait AbstractAdminTokens: Sync + Send { async fn admin_token_revoke(&self, token_id: &str) -> Result<()>; async fn admin_token_authenticate(&self, token: &str) -> Result; + + async fn admin_token_fetch(&self, id: &str) -> Result; } diff --git a/crates/core/database/src/models/admin_tokens/ops/mongodb.rs b/crates/core/database/src/models/admin_tokens/ops/mongodb.rs index ee47bb3a..58b5d2c9 100644 --- a/crates/core/database/src/models/admin_tokens/ops/mongodb.rs +++ b/crates/core/database/src/models/admin_tokens/ops/mongodb.rs @@ -27,4 +27,8 @@ impl AbstractAdminTokens for MongoDb { query!(self, find_one, COL, doc! {"token": token})? .ok_or_else(|| create_error!(InvalidCredentials)) } + + async fn admin_token_fetch(&self, id: &str) -> Result { + query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound)) + } } diff --git a/crates/core/database/src/models/admin_tokens/ops/reference.rs b/crates/core/database/src/models/admin_tokens/ops/reference.rs index fd1697ee..4d28d09b 100644 --- a/crates/core/database/src/models/admin_tokens/ops/reference.rs +++ b/crates/core/database/src/models/admin_tokens/ops/reference.rs @@ -41,4 +41,13 @@ impl AbstractAdminTokens for ReferenceDb { Ok(result.ok_or_else(|| create_error!(NotFound))?) } + + async fn admin_token_fetch(&self, id: &str) -> Result { + let admin_tokens = self.admin_tokens.lock().await; + let result = admin_tokens.iter().find(|tok| tok.0 == id); + + Ok(result + .map(|t| t.1.clone()) + .ok_or_else(|| create_error!(NotFound))?) + } } diff --git a/crates/core/database/src/models/admin_tokens/rocket.rs b/crates/core/database/src/models/admin_tokens/rocket.rs index ed5b771b..34a2d7aa 100644 --- a/crates/core/database/src/models/admin_tokens/rocket.rs +++ b/crates/core/database/src/models/admin_tokens/rocket.rs @@ -1,8 +1,10 @@ +use iso8601_timestamp::Timestamp; +use revolt_config::{capture_internal_error, report_error}; use revolt_result::Result; use rocket::http::Status; use rocket::request::{self, FromRequest, Outcome, Request}; -use crate::{AdminMachineToken, Database}; +use crate::{AdminAuthorization, AdminMachineToken, AdminUser, Database}; #[rocket::async_trait] impl<'r> FromRequest<'r> for AdminMachineToken { @@ -50,3 +52,95 @@ impl<'r> FromRequest<'r> for AdminMachineToken { } } } + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for AdminAuthorization { + type Error = authifier::Error; + + async fn from_request(request: &'r Request<'_>) -> request::Outcome { + let machine: &Option = request + .local_cache_async(async { + let db = request.rocket().state::().expect("`Database`"); + + if let Some(token) = request + .headers() + .get("x-admin-machine") + .next() + .map(|x| x.to_string()) + { + if let Some(on_behalf_of) = request + .headers() + .get("x-admin-on-behalf-of") + .next() + .map(|x| x.to_string()) + { + let config = revolt_config::config().await; + let token = token.to_string(); + if config.api.security.admin_keys.contains(&token) { + let resp: Result = if on_behalf_of.contains("@") { + AdminMachineToken::new_from_email(&on_behalf_of, db).await + } else { + AdminMachineToken::new_from_id(&on_behalf_of, db).await + }; + if let Ok(resp) = resp { + return Some(resp); + } + } + } + } + None + }) + .await; + + if let Some(machine) = machine { + if !machine.on_behalf_of.active { + Outcome::Error((Status::Unauthorized, authifier::Error::LockedOut)) + } else { + Outcome::Success(AdminAuthorization::AdminMachine(machine.clone())) + } + } else { + let user: &Option = request + .local_cache_async(async { + let db = request.rocket().state::().expect("`Database`"); + + let token = request + .headers() + .get("x-admin-user") + .next() + .map(|x| x.to_string()); + + if let Some(token) = token { + if let Ok(admin_token) = db.admin_token_authenticate(&token).await { + if let Some(expiry) = Timestamp::parse(&admin_token.expiry) { + if expiry < Timestamp::now_utc() { + if let Err(e) = db.admin_token_revoke(&admin_token.id).await { + capture_internal_error!(e); + } + return None; + } else if let Ok(user) = + db.admin_user_fetch(&admin_token.user_id).await + { + if !user.active { + return None; + } + return Some(user); + } + } + } + } + None + }) + .await; + + if let Some(user) = user { + if !user.active { + Outcome::Error((Status::Unauthorized, authifier::Error::LockedOut)) + } else { + Outcome::Success(AdminAuthorization::AdminUser(user.clone())) + } + } else { + Outcome::Error((Status::Unauthorized, authifier::Error::InvalidCredentials)) + } + } + } +} diff --git a/crates/core/database/src/models/admin_tokens/schema.rs b/crates/core/database/src/models/admin_tokens/schema.rs new file mode 100644 index 00000000..abfb7089 --- /dev/null +++ b/crates/core/database/src/models/admin_tokens/schema.rs @@ -0,0 +1,59 @@ +use revolt_okapi::openapi3::{SecurityScheme, SecuritySchemeData}; +use revolt_rocket_okapi::{ + gen::OpenApiGenerator, + request::{OpenApiFromRequest, RequestHeaderInput}, +}; + +use crate::{AdminAuthorization, AdminMachineToken}; + +impl OpenApiFromRequest<'_> for AdminAuthorization { + fn from_request_input( + _gen: &mut OpenApiGenerator, + _name: String, + _required: bool, + ) -> revolt_rocket_okapi::Result { + let mut requirements = schemars::Map::new(); + requirements.insert("Admin Token".to_owned(), vec![]); + + Ok(RequestHeaderInput::Security( + "Admin Token".to_owned(), + SecurityScheme { + data: SecuritySchemeData::ApiKey { + name: "x-admin-user".to_owned(), + location: "header".to_owned(), + }, + description: Some("Used to authenticate as an admin user. + Can instead use an x-admin-machine token with an x-on-behalf-of containing the userid or email of + the user the machine is performing the action for.".to_owned()), + extensions: schemars::Map::new(), + }, + requirements, + )) + } +} + +impl OpenApiFromRequest<'_> for AdminMachineToken { + fn from_request_input( + _gen: &mut OpenApiGenerator, + _name: String, + _required: bool, + ) -> revolt_rocket_okapi::Result { + let mut requirements = schemars::Map::new(); + requirements.insert("Admin Machine Token".to_owned(), vec![]); + + Ok(RequestHeaderInput::Security( + "Admin Machine Token".to_owned(), + SecurityScheme { + data: SecuritySchemeData::ApiKey { + name: "x-admin-machine".to_owned(), + location: "header".to_owned(), + }, + description: Some("Used by machines to authenticate on behalf of a user. + Machines are trusted devices that authenticate as other users via providing the x-on-behalf-of header + with an admin user's ID or email.".to_owned()), + extensions: schemars::Map::new(), + }, + requirements, + )) + } +} diff --git a/crates/core/database/src/models/admin_users/axum.rs b/crates/core/database/src/models/admin_users/axum.rs deleted file mode 100644 index f3dc8676..00000000 --- a/crates/core/database/src/models/admin_users/axum.rs +++ /dev/null @@ -1,19 +0,0 @@ -use axum::{extract::FromRequestParts, http::request::Parts}; - -use revolt_result::{create_error, Error, Result}; - -use crate::{AdminUser, Database}; - -#[async_trait::async_trait] -impl FromRequestParts for AdminUser { - type Rejection = Error; - - async fn from_request_parts(parts: &mut Parts, db: &Database) -> Result { - if let Some(Ok(token)) = parts.headers.get("x-admin-user").map(|v| v.to_str()) { - let session = db.admin_token_authenticate(token).await?; - db.admin_user_fetch(&session.user_id).await - } else { - Err(create_error!(NotAuthenticated)) - } - } -} diff --git a/crates/core/database/src/models/admin_users/mod.rs b/crates/core/database/src/models/admin_users/mod.rs index 7b1a1988..23bf00b6 100644 --- a/crates/core/database/src/models/admin_users/mod.rs +++ b/crates/core/database/src/models/admin_users/mod.rs @@ -1,12 +1,8 @@ -#[cfg(feature = "axum-impl")] -mod axum; mod models; mod ops; #[cfg(feature = "rocket-impl")] mod rocket; -#[cfg(feature = "axum-impl")] -pub use self::axum::*; #[cfg(feature = "rocket-impl")] pub use self::rocket::*; pub use models::*; diff --git a/crates/core/database/src/models/admin_users/models.rs b/crates/core/database/src/models/admin_users/models.rs index a931331b..58d3acad 100644 --- a/crates/core/database/src/models/admin_users/models.rs +++ b/crates/core/database/src/models/admin_users/models.rs @@ -39,3 +39,31 @@ impl AdminUser { return db.admin_user_fetch_email(email).await; } } + +pub struct AdminUserPermissionFlagsValue(pub u64); + +impl AdminUserPermissionFlagsValue { + pub fn has(&self, flag: revolt_models::v0::AdminUserPermissionFlags) -> bool { + self.has_value(flag as u64) + } + pub fn has_value(&self, bit: u64) -> bool { + let mask = 1 << bit; + self.0 & mask == mask + } + + pub fn set( + &mut self, + flag: revolt_models::v0::AdminUserPermissionFlags, + toggle: bool, + ) -> &mut Self { + self.set_value(flag as u64, toggle) + } + pub fn set_value(&mut self, bit: u64, toggle: bool) -> &mut Self { + if toggle { + self.0 |= 1 << bit; + } else { + self.0 &= !(1 << bit); + } + self + } +} diff --git a/crates/core/database/src/models/admin_users/ops.rs b/crates/core/database/src/models/admin_users/ops.rs index 044f380d..046c4c8a 100644 --- a/crates/core/database/src/models/admin_users/ops.rs +++ b/crates/core/database/src/models/admin_users/ops.rs @@ -15,4 +15,6 @@ pub trait AbstractAdminUsers: Sync + Send { async fn admin_user_fetch_email(&self, email: &str) -> Result; async fn admin_user_list(&self) -> Result>; + + async fn admin_user_count(&self) -> Result; } diff --git a/crates/core/database/src/models/admin_users/ops/mongodb.rs b/crates/core/database/src/models/admin_users/ops/mongodb.rs index 2f6fd552..b694ea82 100644 --- a/crates/core/database/src/models/admin_users/ops/mongodb.rs +++ b/crates/core/database/src/models/admin_users/ops/mongodb.rs @@ -28,4 +28,8 @@ impl AbstractAdminUsers for MongoDb { async fn admin_user_list(&self) -> Result> { query!(self, find, COL, doc! {}) } + + async fn admin_user_count(&self) -> Result { + query!(self, count_documents, COL, doc! {}).map(|resp| resp as u16) + } } diff --git a/crates/core/database/src/models/admin_users/ops/reference.rs b/crates/core/database/src/models/admin_users/ops/reference.rs index dd75cdb4..1989b2d5 100644 --- a/crates/core/database/src/models/admin_users/ops/reference.rs +++ b/crates/core/database/src/models/admin_users/ops/reference.rs @@ -49,4 +49,8 @@ impl AbstractAdminUsers for ReferenceDb { let admin_users = self.admin_users.lock().await; Ok(admin_users.iter().map(|(_, u)| u).cloned().collect()) } + + async fn admin_user_count(&self) -> Result { + Ok(self.admin_users.lock().await.len() as u16) + } } diff --git a/crates/core/database/src/models/admin_users/rocket.rs b/crates/core/database/src/models/admin_users/rocket.rs index ca738b81..acba6691 100644 --- a/crates/core/database/src/models/admin_users/rocket.rs +++ b/crates/core/database/src/models/admin_users/rocket.rs @@ -31,9 +31,13 @@ impl<'r> FromRequest<'r> for AdminUser { .await; if let Some(user) = user { - Outcome::Success(user.clone()) + if !user.active { + Outcome::Error((Status::Unauthorized, authifier::Error::LockedOut)) + } else { + Outcome::Success(user.clone()) + } } else { - Outcome::Error((Status::Unauthorized, authifier::Error::InvalidSession)) + Outcome::Error((Status::Unauthorized, authifier::Error::InvalidCredentials)) } } } diff --git a/crates/core/database/src/util/bridge/v0.rs b/crates/core/database/src/util/bridge/v0.rs index f741144b..d0cd5521 100644 --- a/crates/core/database/src/util/bridge/v0.rs +++ b/crates/core/database/src/util/bridge/v0.rs @@ -618,6 +618,7 @@ impl From for Report { additional_context: value.additional_context, status: value.status, notes: value.notes, + case_id: value.case_id, } } } @@ -1386,3 +1387,72 @@ impl From for crate::FieldsMessage { } } } + +impl From for AdminUser { + fn from(value: crate::AdminUser) -> Self { + AdminUser { + id: value.id, + platform_user_id: value.platform_user_id, + email: value.email, + active: value.active, + permissions: value.permissions, + revolt_user: None, + } + } +} + +impl From for crate::AdminUser { + fn from(value: AdminUser) -> Self { + crate::AdminUser { + id: value.id, + platform_user_id: value.platform_user_id, + email: value.email, + active: value.active, + permissions: value.permissions, + } + } +} + +impl From for crate::PartialAdminUser { + fn from(value: AdminUserEdit) -> Self { + crate::PartialAdminUser { + id: None, + platform_user_id: value.platform_user_id, + email: value.email, + active: value.active, + permissions: value.permissions, + } + } +} + +impl From for crate::AdminToken { + fn from(value: AdminToken) -> Self { + crate::AdminToken { + id: value.id, + user_id: value.user_id, + token: value.token, + expiry: value.expiry.format_short().to_string(), + } + } +} + +impl From for AdminToken { + fn from(value: crate::AdminToken) -> Self { + AdminToken { + id: value.id, + user_id: value.user_id, + token: value.token, + expiry: Timestamp::parse(&value.expiry).unwrap(), // if it's invalid it shouldn't have made it to this point. + } + } +} + +impl From for AdminObjectNote { + fn from(value: crate::AdminObjectNote) -> Self { + AdminObjectNote { + edited_at: Timestamp::parse(&value.edited_at).unwrap(), // if it's invalid it shouldn't have made it to this point. + last_edited_by_id: value.last_edited_by_id, + content: value.content, + } + } +} diff --git a/crates/core/database/src/util/reference.rs b/crates/core/database/src/util/reference.rs index 9282d39d..f3d4537d 100644 --- a/crates/core/database/src/util/reference.rs +++ b/crates/core/database/src/util/reference.rs @@ -10,7 +10,8 @@ use schemars::{ }; use crate::{ - Bot, Channel, Database, Emoji, Invite, Member, Message, Server, ServerBan, User, Webhook, + AdminToken, Bot, Channel, Database, Emoji, Invite, Member, Message, Server, ServerBan, User, + Webhook, }; /// Reference to some object in the database @@ -26,6 +27,11 @@ impl Reference { Reference { id } } + /// Fetch Admin Token from Ref + pub async fn as_admin_token(&self, db: &Database) -> Result { + db.admin_token_fetch(&self.id).await + } + /// Fetch ban from Ref pub async fn as_ban(&self, db: &Database, server: &str) -> Result { db.fetch_ban(server, &self.id).await diff --git a/crates/core/models/src/v0/admin.rs b/crates/core/models/src/v0/admin.rs index 2dd0668c..ed8a6a07 100644 --- a/crates/core/models/src/v0/admin.rs +++ b/crates/core/models/src/v0/admin.rs @@ -1,6 +1,6 @@ use iso8601_timestamp::Timestamp; -use crate::v0::Report; +use crate::v0::{Report, User}; auto_derived! { #[derive(Default)] @@ -211,9 +211,6 @@ auto_derived! { #[cfg_attr(feature = "validator", derive(validator::Validate))] pub struct AdminTokenCreate { - /// the user who this token is for - #[cfg_attr(feature = "serde", serde(rename="user"))] - pub user_id: String, /// The expiry timestamp for this token, in iso6801. Max 30 days from current time. pub expiry: Timestamp, } @@ -229,6 +226,9 @@ auto_derived! { pub active: bool, /// The permissions of the user pub permissions: u64, + /// The Revolt user attached to this user. Use this for data like names and avatars. + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub revolt_user: Option } #[cfg_attr(feature = "validator", derive(validator::Validate))] @@ -258,4 +258,31 @@ auto_derived! { /// The permissions of the user pub permissions: Option, } + + #[repr(u64)] + pub enum AdminUserPermissionFlags { + ObjectNotes = 0, + + ManageAdminUsers = 1, + CreateTokens = 2, + + ViewUsers = 3, + ManageUsers = 4, + ManageUsersSenstiveInfo = 5, + + ViewServers = 6, + ManageServers = 7, + ViewDMChannels = 8, + ManageDMChannels = 9, + + ViewCases = 10, + ManageOwnCases = 11, + ManageOtherCases = 12, + + ViewReports = 13, + + Search = 14, + ManageNotes = 15, + Discover = 16, + } } diff --git a/crates/delta/src/routes/admin/accounts/account_delete.rs b/crates/delta/src/routes/admin/accounts/account_delete.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/accounts/account_disable.rs b/crates/delta/src/routes/admin/accounts/account_disable.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/accounts/account_email_change.rs b/crates/delta/src/routes/admin/accounts/account_email_change.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/accounts/account_email_verify.rs b/crates/delta/src/routes/admin/accounts/account_email_verify.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/accounts/account_enable.rs b/crates/delta/src/routes/admin/accounts/account_enable.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/accounts/account_mfa_reset.rs b/crates/delta/src/routes/admin/accounts/account_mfa_reset.rs new file mode 100644 index 00000000..9d1a4a56 --- /dev/null +++ b/crates/delta/src/routes/admin/accounts/account_mfa_reset.rs @@ -0,0 +1 @@ +// This should allow resetting of all factors or individual ones. diff --git a/crates/delta/src/routes/admin/accounts/account_reset_lockout.rs b/crates/delta/src/routes/admin/accounts/account_reset_lockout.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/accounts/mod.rs b/crates/delta/src/routes/admin/accounts/mod.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/crates/delta/src/routes/admin/accounts/mod.rs @@ -0,0 +1 @@ + diff --git a/crates/delta/src/routes/admin/cases/actions/case_add_comment.rs b/crates/delta/src/routes/admin/cases/actions/case_add_comment.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/crates/delta/src/routes/admin/cases/actions/case_add_comment.rs @@ -0,0 +1 @@ + diff --git a/crates/delta/src/routes/admin/cases/actions/case_create.rs b/crates/delta/src/routes/admin/cases/actions/case_create.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/cases/actions/case_edit_tags.rs b/crates/delta/src/routes/admin/cases/actions/case_edit_tags.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/cases/actions/case_rename.rs b/crates/delta/src/routes/admin/cases/actions/case_rename.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/cases/actions/case_send_notification.rs b/crates/delta/src/routes/admin/cases/actions/case_send_notification.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/cases/actions/case_state.rs b/crates/delta/src/routes/admin/cases/actions/case_state.rs new file mode 100644 index 00000000..64058a86 --- /dev/null +++ b/crates/delta/src/routes/admin/cases/actions/case_state.rs @@ -0,0 +1 @@ +//close/reopen a case diff --git a/crates/delta/src/routes/admin/cases/actions/mod.rs b/crates/delta/src/routes/admin/cases/actions/mod.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/cases/fetch/case_get.rs b/crates/delta/src/routes/admin/cases/fetch/case_get.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/crates/delta/src/routes/admin/cases/fetch/case_get.rs @@ -0,0 +1 @@ + diff --git a/crates/delta/src/routes/admin/cases/fetch/case_get_all_open.rs b/crates/delta/src/routes/admin/cases/fetch/case_get_all_open.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/cases/fetch/mod.rs b/crates/delta/src/routes/admin/cases/fetch/mod.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/cases/mod.rs b/crates/delta/src/routes/admin/cases/mod.rs new file mode 100644 index 00000000..f6d4d6e6 --- /dev/null +++ b/crates/delta/src/routes/admin/cases/mod.rs @@ -0,0 +1,2 @@ +mod actions; +mod fetch; diff --git a/crates/delta/src/routes/admin/channels/actions/channel_delete.rs b/crates/delta/src/routes/admin/channels/actions/channel_delete.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/channels/actions/channel_edit.rs b/crates/delta/src/routes/admin/channels/actions/channel_edit.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/channels/actions/channel_wipe.rs b/crates/delta/src/routes/admin/channels/actions/channel_wipe.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/channels/actions/mod.rs b/crates/delta/src/routes/admin/channels/actions/mod.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/channels/fetch/channel_get.rs b/crates/delta/src/routes/admin/channels/fetch/channel_get.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/channels/fetch/channel_get_participants.rs b/crates/delta/src/routes/admin/channels/fetch/channel_get_participants.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/channels/fetch/mod.rs b/crates/delta/src/routes/admin/channels/fetch/mod.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/channels/mod.rs b/crates/delta/src/routes/admin/channels/mod.rs new file mode 100644 index 00000000..f6d4d6e6 --- /dev/null +++ b/crates/delta/src/routes/admin/channels/mod.rs @@ -0,0 +1,2 @@ +mod actions; +mod fetch; diff --git a/crates/delta/src/routes/admin/messages/delete.rs b/crates/delta/src/routes/admin/messages/delete.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/meta/create_token.rs b/crates/delta/src/routes/admin/meta/create_token.rs new file mode 100644 index 00000000..11f16d20 --- /dev/null +++ b/crates/delta/src/routes/admin/meta/create_token.rs @@ -0,0 +1,38 @@ +use iso8601_timestamp::{Duration, Timestamp}; +use revolt_database::{AdminMachineToken, AdminToken, Database}; +use revolt_models::v0::{self}; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; + +use crate::routes::admin::util::user_has_permission; + +/// Create a token for your account. Must have the CreateTokens permission +#[openapi(tag = "Admin")] +#[post("/tokens", data = "")] +pub async fn admin_create_token( + db: &State, + auth: AdminMachineToken, + body: Json, +) -> Result> { + if !user_has_permission( + &auth.on_behalf_of, + v0::AdminUserPermissionFlags::CreateTokens, + ) { + return Err(create_error!(NotFound)); + } + + if body.expiry > Timestamp::now_utc() + Duration::days(30) { + return Err(create_error!(FailedValidation { + error: "The expiry is more than 30 days away.".to_string() + })); + } + + let token = AdminToken::new(&auth.on_behalf_of.id, body.expiry); + db.admin_token_create(token.clone()).await?; + Ok(Json(v0::AdminToken { + id: token.id, + user_id: token.user_id, + token: token.token, + expiry: body.expiry, + })) +} diff --git a/crates/delta/src/routes/admin/meta/create_user.rs b/crates/delta/src/routes/admin/meta/create_user.rs new file mode 100644 index 00000000..93e05f56 --- /dev/null +++ b/crates/delta/src/routes/admin/meta/create_user.rs @@ -0,0 +1,26 @@ +use revolt_database::{AdminAuthorization, AdminUser, Database}; +use revolt_models::v0::{self}; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; + +use crate::routes::admin::util::{flatten_authorized_user, user_has_permission}; + +/// Edit an admin user. Requires ManageAdminUsers flag. +#[openapi(tag = "Admin")] +#[post("/users", data = "")] +pub async fn admin_create_user( + db: &State, + auth: AdminAuthorization, + body: Json, +) -> Result> { + let user = flatten_authorized_user(&auth); + if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageAdminUsers) { + return Err(create_error!(NotFound)); + } + + // TODO: technically there's a privilege escalation here since anyone with the manageAdminUsers permission can assign whatever permissions they want. + // But this is built with the assumption that people with ManageAdminUsers are already privileged. + let user = AdminUser::new(&body.email, &body.platform_user_id, body.permissions); + db.admin_user_insert(user.clone()).await?; + Ok(Json(user.into())) +} diff --git a/crates/delta/src/routes/admin/meta/edit_user.rs b/crates/delta/src/routes/admin/meta/edit_user.rs new file mode 100644 index 00000000..4d138cbf --- /dev/null +++ b/crates/delta/src/routes/admin/meta/edit_user.rs @@ -0,0 +1,27 @@ +use revolt_database::{util::reference::Reference, AdminAuthorization, Database}; +use revolt_models::v0::{self}; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; +use rocket_empty::EmptyResponse; + +use crate::routes::admin::util::{flatten_authorized_user, user_has_permission}; + +/// Edit an admin user. Requires ManageAdminUsers flag. +#[openapi(tag = "Admin")] +#[patch("/users/", data = "")] +pub async fn admin_edit_user( + db: &State, + auth: AdminAuthorization, + target: Reference, + body: Json, +) -> Result { + let user = flatten_authorized_user(&auth); + if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageAdminUsers) { + return Err(create_error!(NotFound)); + } + + // TODO: technically there's a privilege escalation here since anyone with the manageAdminUsers permission can assign whatever permissions they want. + // But this is built with the assumption that people with ManageAdminUsers are already privileged. + db.admin_user_update(&target.id, body.0.into()).await?; + Ok(EmptyResponse) +} diff --git a/crates/delta/src/routes/admin/meta/fetch_users.rs b/crates/delta/src/routes/admin/meta/fetch_users.rs new file mode 100644 index 00000000..68f62c45 --- /dev/null +++ b/crates/delta/src/routes/admin/meta/fetch_users.rs @@ -0,0 +1,29 @@ +use revolt_database::{AdminAuthorization, Database}; +use revolt_models::v0::AdminUser; +use revolt_result::Result; +use rocket::{serde::json::Json, State}; + +/// Get a list of admin users. Any active user may use this endpoint. +/// Typically the client should cache this data. +#[openapi(tag = "Admin")] +#[get("/users")] +pub async fn admin_fetch_users( + db: &State, + auth: AdminAuthorization, +) -> Result>> { + let users = db.admin_user_list().await?; + let userids: Vec = users.iter().map(|u| u.platform_user_id.clone()).collect(); + let revolt_users = db.fetch_users(&userids).await?; + + let mut resp = vec![]; + for admin_user in users { + let mut user = AdminUser::from(admin_user); + user.revolt_user = match revolt_users.iter().find(|ru| ru.id == user.id) { + Some(some_user) => Some(some_user.clone().into_self(false).await), + None => None, + }; + resp.push(user); + } + + Ok(Json(resp)) +} diff --git a/crates/delta/src/routes/admin/meta/mod.rs b/crates/delta/src/routes/admin/meta/mod.rs new file mode 100644 index 00000000..8d2cbac0 --- /dev/null +++ b/crates/delta/src/routes/admin/meta/mod.rs @@ -0,0 +1,5 @@ +pub mod create_token; +pub mod create_user; +pub mod edit_user; +pub mod fetch_users; +pub mod revoke_token; diff --git a/crates/delta/src/routes/admin/meta/revoke_token.rs b/crates/delta/src/routes/admin/meta/revoke_token.rs new file mode 100644 index 00000000..7df8ad19 --- /dev/null +++ b/crates/delta/src/routes/admin/meta/revoke_token.rs @@ -0,0 +1,39 @@ +use revolt_database::{util::reference::Reference, AdminMachineToken, Database}; +use revolt_models::v0::{self}; +use revolt_result::{create_error, Result}; +use rocket::State; +use rocket_empty::EmptyResponse; + +use crate::routes::admin::util::user_has_permission; + +/// Revoke a token. Must be your own, or you must have the ManageAdminUsers permission to revoke other's tokens. +/// You must have the CreateTokens permission. +#[openapi(tag = "Admin")] +#[delete("/tokens/")] +pub async fn admin_revoke_token( + db: &State, + auth: AdminMachineToken, + token: Reference, +) -> Result { + if !user_has_permission( + &auth.on_behalf_of, + v0::AdminUserPermissionFlags::CreateTokens, + ) { + return Err(create_error!(NotFound)); + } + + let token = token.as_admin_token(db).await?; + + // only own user and those with ManageAdminUsers can revoke a token + if token.user_id != auth.on_behalf_of.id + && !user_has_permission( + &auth.on_behalf_of, + v0::AdminUserPermissionFlags::ManageAdminUsers, + ) + { + return Err(create_error!(NotFound)); + } + + db.admin_token_revoke(&token.id).await?; + Ok(EmptyResponse) +} diff --git a/crates/delta/src/routes/admin/mod.rs b/crates/delta/src/routes/admin/mod.rs new file mode 100644 index 00000000..127c1b6d --- /dev/null +++ b/crates/delta/src/routes/admin/mod.rs @@ -0,0 +1,26 @@ +use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi; +use rocket::Route; + +mod accounts; +mod cases; +mod meta; +mod reports; +mod roles; +mod search; +mod users; + +mod object_edit_note; +mod object_get_note; +mod util; + +pub fn routes() -> (Vec, OpenApi) { + openapi_get_routes_spec![ + object_edit_note::admin_object_edit_note, + object_get_note::admin_object_get_note, + meta::create_user::admin_create_user, + meta::edit_user::admin_edit_user, + meta::fetch_users::admin_fetch_users, + meta::create_token::admin_create_token, + meta::revoke_token::admin_revoke_token + ] +} diff --git a/crates/delta/src/routes/admin/object_edit_note.rs b/crates/delta/src/routes/admin/object_edit_note.rs new file mode 100644 index 00000000..fb619f6f --- /dev/null +++ b/crates/delta/src/routes/admin/object_edit_note.rs @@ -0,0 +1,28 @@ +use revolt_database::{util::reference::Reference, AdminAuthorization, AdminObjectNote, Database}; +use revolt_models::v0::{self}; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; +use rocket_empty::EmptyResponse; + +use crate::routes::admin::util::{flatten_authorized_user, user_has_permission}; + +/// Edit an object note. Requires ObjectNotes permission. +#[openapi(tag = "Admin")] +#[post("/notes/", data = "")] +pub async fn admin_object_edit_note( + db: &State, + auth: AdminAuthorization, + object: Reference, + body: Json, +) -> Result { + let user = flatten_authorized_user(&auth); + if !user_has_permission(user, v0::AdminUserPermissionFlags::ObjectNotes) { + return Err(create_error!(NotFound)); + } + + // Unfortunately due to how our system works, we can't limit users to editing objects they have permissions for (eg users, servers, etc.). + // Hence it being tied to its own permission + db.admin_note_update(AdminObjectNote::new(&object.id, &user.id, &body.content)) + .await?; + Ok(EmptyResponse) +} diff --git a/crates/delta/src/routes/admin/object_get_note.rs b/crates/delta/src/routes/admin/object_get_note.rs new file mode 100644 index 00000000..2b3cf0b3 --- /dev/null +++ b/crates/delta/src/routes/admin/object_get_note.rs @@ -0,0 +1,24 @@ +use revolt_database::{util::reference::Reference, AdminAuthorization, Database}; +use revolt_models::v0::{self}; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; + +use crate::routes::admin::util::{flatten_authorized_user, user_has_permission}; + +/// Edit an object note. Requires ObjectNotes permission. +#[openapi(tag = "Admin")] +#[get("/notes/")] +pub async fn admin_object_get_note( + db: &State, + auth: AdminAuthorization, + object: Reference, +) -> Result> { + let user = flatten_authorized_user(&auth); + if !user_has_permission(user, v0::AdminUserPermissionFlags::ObjectNotes) { + return Err(create_error!(NotFound)); + } + + // Unfortunately due to how our system works, we can't limit users to editing objects they have permissions for (eg users, servers, etc.). + // Hence it being tied to its own permission + Ok(Json(db.admin_note_fetch(&object.id).await?.into())) +} diff --git a/crates/delta/src/routes/admin/reports/actions/mod.rs b/crates/delta/src/routes/admin/reports/actions/mod.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/reports/actions/report_assign.rs b/crates/delta/src/routes/admin/reports/actions/report_assign.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/reports/actions/report_create.rs b/crates/delta/src/routes/admin/reports/actions/report_create.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/reports/actions/report_edit.rs b/crates/delta/src/routes/admin/reports/actions/report_edit.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/reports/actions/reports_bulk_assign.rs b/crates/delta/src/routes/admin/reports/actions/reports_bulk_assign.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/reports/fetch/mod.rs b/crates/delta/src/routes/admin/reports/fetch/mod.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/reports/fetch/reports_unassigned.rs b/crates/delta/src/routes/admin/reports/fetch/reports_unassigned.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/reports/mod.rs b/crates/delta/src/routes/admin/reports/mod.rs new file mode 100644 index 00000000..f6d4d6e6 --- /dev/null +++ b/crates/delta/src/routes/admin/reports/mod.rs @@ -0,0 +1,2 @@ +mod actions; +mod fetch; diff --git a/crates/delta/src/routes/admin/roles/mod.rs b/crates/delta/src/routes/admin/roles/mod.rs new file mode 100644 index 00000000..8bc36cc3 --- /dev/null +++ b/crates/delta/src/routes/admin/roles/mod.rs @@ -0,0 +1,3 @@ +mod role_create; +mod role_delete; +mod role_edit; diff --git a/crates/delta/src/routes/admin/roles/role_create.rs b/crates/delta/src/routes/admin/roles/role_create.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/roles/role_delete.rs b/crates/delta/src/routes/admin/roles/role_delete.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/roles/role_edit.rs b/crates/delta/src/routes/admin/roles/role_edit.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/search/mod.rs b/crates/delta/src/routes/admin/search/mod.rs new file mode 100644 index 00000000..643b67e1 --- /dev/null +++ b/crates/delta/src/routes/admin/search/mod.rs @@ -0,0 +1 @@ +mod search_everything; diff --git a/crates/delta/src/routes/admin/search/search_everything.rs b/crates/delta/src/routes/admin/search/search_everything.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/servers/actions/mod.rs b/crates/delta/src/routes/admin/servers/actions/mod.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/servers/actions/server_add_members.rs b/crates/delta/src/routes/admin/servers/actions/server_add_members.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/servers/actions/server_change_owner.rs b/crates/delta/src/routes/admin/servers/actions/server_change_owner.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/servers/actions/server_create_invite.rs b/crates/delta/src/routes/admin/servers/actions/server_create_invite.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/servers/actions/server_delete.rs b/crates/delta/src/routes/admin/servers/actions/server_delete.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/servers/actions/server_delete_invites.rs b/crates/delta/src/routes/admin/servers/actions/server_delete_invites.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/servers/actions/server_edit.rs b/crates/delta/src/routes/admin/servers/actions/server_edit.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/servers/actions/server_remove_members.rs b/crates/delta/src/routes/admin/servers/actions/server_remove_members.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/servers/actions/server_set_flags.rs b/crates/delta/src/routes/admin/servers/actions/server_set_flags.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/servers/fetch/mod.rs b/crates/delta/src/routes/admin/servers/fetch/mod.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/servers/fetch/server_get.rs b/crates/delta/src/routes/admin/servers/fetch/server_get.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/servers/fetch/server_get_participants.rs b/crates/delta/src/routes/admin/servers/fetch/server_get_participants.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/servers/mod.rs b/crates/delta/src/routes/admin/servers/mod.rs new file mode 100644 index 00000000..f6d4d6e6 --- /dev/null +++ b/crates/delta/src/routes/admin/servers/mod.rs @@ -0,0 +1,2 @@ +mod actions; +mod fetch; diff --git a/crates/delta/src/routes/admin/users/actions/mod.rs b/crates/delta/src/routes/admin/users/actions/mod.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/users/actions/user_ban.rs b/crates/delta/src/routes/admin/users/actions/user_ban.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/users/actions/user_data_package.rs b/crates/delta/src/routes/admin/users/actions/user_data_package.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/users/actions/user_reset_profile.rs b/crates/delta/src/routes/admin/users/actions/user_reset_profile.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/users/actions/user_send_alert.rs b/crates/delta/src/routes/admin/users/actions/user_send_alert.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/users/actions/user_strike.rs b/crates/delta/src/routes/admin/users/actions/user_strike.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/users/actions/user_suspend.rs b/crates/delta/src/routes/admin/users/actions/user_suspend.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/users/actions/user_unban.rs b/crates/delta/src/routes/admin/users/actions/user_unban.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/users/actions/user_unsuspend.rs b/crates/delta/src/routes/admin/users/actions/user_unsuspend.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/users/actions/user_wipe_messages.rs b/crates/delta/src/routes/admin/users/actions/user_wipe_messages.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/users/fetch/mod.rs b/crates/delta/src/routes/admin/users/fetch/mod.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/users/fetch/user_get.rs b/crates/delta/src/routes/admin/users/fetch/user_get.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/delta/src/routes/admin/users/mod.rs b/crates/delta/src/routes/admin/users/mod.rs new file mode 100644 index 00000000..f6d4d6e6 --- /dev/null +++ b/crates/delta/src/routes/admin/users/mod.rs @@ -0,0 +1,2 @@ +mod actions; +mod fetch; diff --git a/crates/delta/src/routes/admin/util.rs b/crates/delta/src/routes/admin/util.rs new file mode 100644 index 00000000..0fc72128 --- /dev/null +++ b/crates/delta/src/routes/admin/util.rs @@ -0,0 +1,15 @@ +use revolt_database::{AdminAuthorization, AdminUser, AdminUserPermissionFlagsValue}; +use revolt_models::v0::AdminUserPermissionFlags; + +pub fn user_has_permission(user: &AdminUser, permission: AdminUserPermissionFlags) -> bool { + let flags = AdminUserPermissionFlagsValue(user.permissions); + + flags.has(permission) +} + +pub fn flatten_authorized_user<'a>(auth: &'a AdminAuthorization) -> &'a AdminUser { + match auth { + AdminAuthorization::AdminUser(admin_user) => &admin_user, + AdminAuthorization::AdminMachine(admin_machine_token) => &admin_machine_token.on_behalf_of, + } +} diff --git a/crates/delta/src/routes/mod.rs b/crates/delta/src/routes/mod.rs index 9643a365..dc67158a 100644 --- a/crates/delta/src/routes/mod.rs +++ b/crates/delta/src/routes/mod.rs @@ -4,6 +4,7 @@ pub use rocket::http::Status; pub use rocket::response::Redirect; use rocket::{Build, Rocket}; +mod admin; mod bots; mod channels; mod customisation; @@ -21,89 +22,79 @@ mod webhooks; pub fn mount(config: Settings, mut rocket: Rocket) -> Rocket { let settings = OpenApiSettings::default(); - if config.features.webhooks_enabled { - mount_endpoints_and_merged_docs! { - rocket, "/".to_owned(), settings, - "/" => (vec![], custom_openapi_spec()), - "" => openapi_get_routes_spec![root::root], - "/users" => users::routes(), - "/bots" => bots::routes(), - "/channels" => channels::routes(), - "/servers" => servers::routes(), - "/invites" => invites::routes(), - "/custom" => customisation::routes(), - "/safety" => safety::routes(), - "/auth/account" => rocket_authifier::routes::account::routes(), - "/auth/session" => rocket_authifier::routes::session::routes(), - "/auth/mfa" => rocket_authifier::routes::mfa::routes(), - "/onboard" => onboard::routes(), - "/policy" => policy::routes(), - "/push" => push::routes(), - "/sync" => sync::routes(), - "/webhooks" => webhooks::routes() - }; - } else { - mount_endpoints_and_merged_docs! { - rocket, "/".to_owned(), settings, - "/" => (vec![], custom_openapi_spec()), - "" => openapi_get_routes_spec![root::root], - "/users" => users::routes(), - "/bots" => bots::routes(), - "/channels" => channels::routes(), - "/servers" => servers::routes(), - "/invites" => invites::routes(), - "/custom" => customisation::routes(), - "/safety" => safety::routes(), - "/auth/account" => rocket_authifier::routes::account::routes(), - "/auth/session" => rocket_authifier::routes::session::routes(), - "/auth/mfa" => rocket_authifier::routes::mfa::routes(), - "/onboard" => onboard::routes(), - "/policy" => policy::routes(), - "/push" => push::routes(), - "/sync" => sync::routes() - }; + let mut openapi_list: Vec<(_, revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi)> = + Vec::new(); + + let routes = vec![ + ("/", (vec![], custom_openapi_spec()), true, true), + ("/", openapi_get_routes_spec![root::root], true, true), + ("/users", users::routes(), true, true), + ("/bots", bots::routes(), true, true), + ("/channels", channels::routes(), true, true), + ("/servers", servers::routes(), true, true), + ("/invites", invites::routes(), true, true), + ("/custom", customisation::routes(), true, true), + ("/safety", safety::routes(), true, true), + ( + "/auth/mfa", + rocket_authifier::routes::mfa::routes(), + true, + true, + ), + ("/onboard", onboard::routes(), true, true), + ("/policy", policy::routes(), true, true), + ("/push", push::routes(), true, true), + ("/sync", sync::routes(), true, true), + ( + "/auth/account", + rocket_authifier::routes::account::routes(), + true, + true, + ), + ( + "/auth/session", + rocket_authifier::routes::session::routes(), + true, + true, + ), + ( + "/admin", + admin::routes(), + config.features.admin_api_enabled, + config.features.admin_api_show_spec, + ), + ( + "/webhooks", + webhooks::routes(), + config.features.webhooks_enabled, + true, + ), + ]; + + for (base, (routes, openapi), enabled, show_spec) in routes { + if !enabled { + continue; + } + + rocket = rocket.mount(base, routes); + if show_spec { + openapi_list.push((base, openapi)); + } } - if config.features.webhooks_enabled { - mount_endpoints_and_merged_docs! { - rocket, "/0.8".to_owned(), settings, - "/" => (vec![], custom_openapi_spec()), - "" => openapi_get_routes_spec![root::root], - "/users" => users::routes(), - "/bots" => bots::routes(), - "/channels" => channels::routes(), - "/servers" => servers::routes(), - "/invites" => invites::routes(), - "/custom" => customisation::routes(), - "/safety" => safety::routes(), - "/auth/account" => rocket_authifier::routes::account::routes(), - "/auth/session" => rocket_authifier::routes::session::routes(), - "/auth/mfa" => rocket_authifier::routes::mfa::routes(), - "/onboard" => onboard::routes(), - "/push" => push::routes(), - "/sync" => sync::routes(), - "/webhooks" => webhooks::routes() + let openapi_docs = + match revolt_rocket_okapi::revolt_okapi::merge::marge_spec_list(&openapi_list) { + Ok(docs) => docs, + Err(err) => panic!("Could not merge OpenAPI spec: {}", err), }; - } else { - mount_endpoints_and_merged_docs! { - rocket, "/0.8".to_owned(), settings, - "/" => (vec![], custom_openapi_spec()), - "" => openapi_get_routes_spec![root::root], - "/users" => users::routes(), - "/bots" => bots::routes(), - "/channels" => channels::routes(), - "/servers" => servers::routes(), - "/invites" => invites::routes(), - "/custom" => customisation::routes(), - "/safety" => safety::routes(), - "/auth/account" => rocket_authifier::routes::account::routes(), - "/auth/session" => rocket_authifier::routes::session::routes(), - "/auth/mfa" => rocket_authifier::routes::mfa::routes(), - "/onboard" => onboard::routes(), - "/push" => push::routes(), - "/sync" => sync::routes() - }; - } + + rocket = rocket.mount( + "/", + vec![revolt_rocket_okapi::get_openapi_route( + openapi_docs, + &settings, + )], + ); rocket } @@ -238,11 +229,6 @@ fn custom_openapi_spec() -> OpenApi { description: Some("Local Revolt Environment".to_owned()), ..Default::default() }, - Server { - url: "http://local.revolt.chat:14702/0.8".to_owned(), - description: Some("Local Revolt Environment (v0.8)".to_owned()), - ..Default::default() - }, ], external_docs: Some(ExternalDocs { url: "https://developers.revolt.chat".to_owned(), @@ -251,6 +237,13 @@ fn custom_openapi_spec() -> OpenApi { }), extensions, tags: vec![ + Tag { + name: "Admin".to_owned(), + description: Some( + "Used when running your own instance to manage and moderate".to_owned(), + ), + ..Default::default() + }, Tag { name: "Core".to_owned(), description: Some( diff --git a/crates/delta/src/routes/safety/report_content.rs b/crates/delta/src/routes/safety/report_content.rs index 1d5c2356..c1642ea0 100644 --- a/crates/delta/src/routes/safety/report_content.rs +++ b/crates/delta/src/routes/safety/report_content.rs @@ -123,6 +123,7 @@ pub async fn report_content( additional_context: data.additional_context, status: ReportStatus::Created {}, notes: String::new(), + case_id: None, }; db.insert_report(&report).await?; From bf4530563a59b92922876454ae6b8bace1994afe Mon Sep 17 00:00:00 2001 From: IAmTomahawkx Date: Tue, 22 Jul 2025 03:27:05 -0700 Subject: [PATCH 4/4] feat: Working server/comment routes --- crates/core/database/src/drivers/reference.rs | 11 +- .../src/models/admin_audits/models.rs | 10 +- .../src/models/admin_case_comments/models.rs | 54 ------- .../src/models/admin_case_comments/ops.rs | 19 --- .../models/admin_case_comments/ops/mongodb.rs | 36 ----- .../admin_case_comments/ops/reference.rs | 49 ------ .../database/src/models/admin_cases/ops.rs | 2 + .../src/models/admin_cases/ops/mongodb.rs | 5 + .../src/models/admin_cases/ops/reference.rs | 9 ++ .../mod.rs | 0 .../src/models/admin_comments/models.rs | 95 +++++++++++ .../database/src/models/admin_comments/ops.rs | 28 ++++ .../src/models/admin_comments/ops/mongodb.rs | 47 ++++++ .../models/admin_comments/ops/reference.rs | 73 +++++++++ .../models/admin_migrations/ops/mongodb.rs | 14 ++ .../admin_migrations/ops/mongodb/init.rs | 132 +++++++++++++++ .../database/src/models/admin_notes/mod.rs | 5 - .../database/src/models/admin_notes/models.rs | 35 ---- .../database/src/models/admin_notes/ops.rs | 12 -- .../src/models/admin_notes/ops/mongodb.rs | 34 ---- .../src/models/admin_notes/ops/reference.rs | 29 ---- .../src/models/channel_invites/ops/mongodb.rs | 9 +- crates/core/database/src/models/mod.rs | 9 +- .../src/models/server_members/model.rs | 25 +-- .../database/src/models/server_members/ops.rs | 14 ++ .../src/models/server_members/ops/mongodb.rs | 86 +++++++++- .../models/server_members/ops/reference.rs | 90 +++++++++++ crates/core/database/src/util/bridge/v0.rs | 36 ++++- crates/core/database/src/util/reference.rs | 9 +- crates/core/models/src/v0/admin.rs | 150 ++++++++++++++---- crates/core/models/src/v0/server_members.rs | 5 + crates/core/result/src/axum.rs | 2 + crates/core/result/src/lib.rs | 2 + crates/core/result/src/rocket.rs | 2 + .../routes/admin/comments/comment_create.rs | 44 +++++ .../src/routes/admin/comments/comment_edit.rs | 56 +++++++ .../admin/comments/comment_fetch_case.rs | 43 +++++ .../admin/comments/comment_fetch_object.rs | 43 +++++ crates/delta/src/routes/admin/comments/mod.rs | 4 + .../src/routes/admin/meta/create_token.rs | 4 +- .../src/routes/admin/meta/create_user.rs | 4 +- .../delta/src/routes/admin/meta/edit_user.rs | 4 +- .../src/routes/admin/meta/revoke_token.rs | 4 +- crates/delta/src/routes/admin/mod.rs | 24 ++- .../src/routes/admin/object_edit_note.rs | 28 ---- .../delta/src/routes/admin/object_get_note.rs | 24 --- .../src/routes/admin/servers/actions/mod.rs | 9 ++ .../servers/actions/server_add_members.rs | 48 ++++++ .../servers/actions/server_ban_members.rs | 83 ++++++++++ .../servers/actions/server_change_owner.rs | 55 +++++++ .../servers/actions/server_create_invite.rs | 66 ++++++++ .../admin/servers/actions/server_delete.rs | 41 +++++ .../servers/actions/server_delete_invites.rs | 62 ++++++++ .../admin/servers/actions/server_edit.rs | 52 ++++++ .../servers/actions/server_remove_members.rs | 61 +++++++ .../admin/servers/actions/server_set_flags.rs | 0 .../servers/actions/server_unban_members.rs | 60 +++++++ .../src/routes/admin/servers/fetch/mod.rs | 3 + .../routes/admin/servers/fetch/server_get.rs | 51 ++++++ .../servers/fetch/server_get_all_members.rs | 67 ++++++++ .../servers/fetch/server_get_participants.rs | 51 ++++++ crates/delta/src/routes/admin/servers/mod.rs | 4 +- crates/delta/src/routes/admin/util.rs | 43 ++++- crates/delta/src/routes/bots/invite.rs | 2 +- .../delta/src/routes/invites/invite_join.rs | 2 +- crates/delta/src/routes/servers/mod.rs | 2 +- .../delta/src/routes/servers/server_create.rs | 2 +- .../delta/src/routes/servers/server_edit.rs | 14 +- crates/delta/src/util/test.rs | 2 +- scripts/email_users_via_zammad.py | 52 ++++++ 70 files changed, 1841 insertions(+), 411 deletions(-) delete mode 100644 crates/core/database/src/models/admin_case_comments/models.rs delete mode 100644 crates/core/database/src/models/admin_case_comments/ops.rs delete mode 100644 crates/core/database/src/models/admin_case_comments/ops/mongodb.rs delete mode 100644 crates/core/database/src/models/admin_case_comments/ops/reference.rs rename crates/core/database/src/models/{admin_case_comments => admin_comments}/mod.rs (100%) create mode 100644 crates/core/database/src/models/admin_comments/models.rs create mode 100644 crates/core/database/src/models/admin_comments/ops.rs create mode 100644 crates/core/database/src/models/admin_comments/ops/mongodb.rs create mode 100644 crates/core/database/src/models/admin_comments/ops/reference.rs delete mode 100644 crates/core/database/src/models/admin_notes/mod.rs delete mode 100644 crates/core/database/src/models/admin_notes/models.rs delete mode 100644 crates/core/database/src/models/admin_notes/ops.rs delete mode 100644 crates/core/database/src/models/admin_notes/ops/mongodb.rs delete mode 100644 crates/core/database/src/models/admin_notes/ops/reference.rs create mode 100644 crates/delta/src/routes/admin/comments/comment_create.rs create mode 100644 crates/delta/src/routes/admin/comments/comment_edit.rs create mode 100644 crates/delta/src/routes/admin/comments/comment_fetch_case.rs create mode 100644 crates/delta/src/routes/admin/comments/comment_fetch_object.rs create mode 100644 crates/delta/src/routes/admin/comments/mod.rs delete mode 100644 crates/delta/src/routes/admin/object_edit_note.rs delete mode 100644 crates/delta/src/routes/admin/object_get_note.rs create mode 100644 crates/delta/src/routes/admin/servers/actions/server_ban_members.rs delete mode 100644 crates/delta/src/routes/admin/servers/actions/server_set_flags.rs create mode 100644 crates/delta/src/routes/admin/servers/actions/server_unban_members.rs create mode 100644 crates/delta/src/routes/admin/servers/fetch/server_get_all_members.rs create mode 100644 scripts/email_users_via_zammad.py diff --git a/crates/core/database/src/drivers/reference.rs b/crates/core/database/src/drivers/reference.rs index 6587b851..680e7959 100644 --- a/crates/core/database/src/drivers/reference.rs +++ b/crates/core/database/src/drivers/reference.rs @@ -6,10 +6,10 @@ use std::{ use futures::lock::Mutex; use crate::{ - AdminAuditItem, AdminCase, AdminCaseComment, AdminObjectNote, AdminStrike, AdminToken, - AdminUser, Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, FileHash, Invite, - Member, MemberCompositeKey, Message, PolicyChange, RatelimitEvent, Report, Server, ServerBan, - Snapshot, User, UserSettings, Webhook, + AdminAuditItem, AdminCase, AdminComment, AdminStrike, AdminToken, AdminUser, Bot, Channel, + ChannelCompositeKey, ChannelUnread, Emoji, File, FileHash, Invite, Member, MemberCompositeKey, + Message, PolicyChange, RatelimitEvent, Report, Server, ServerBan, Snapshot, User, UserSettings, + Webhook, }; database_derived!( @@ -18,8 +18,7 @@ database_derived!( pub struct ReferenceDb { pub admin_audits: Arc>>, pub admin_cases: Arc>>, - pub admin_case_comments: Arc>>, - pub admin_object_notes: Arc>>, + pub admin_comments: Arc>>, pub admin_strikes: Arc>>, pub admin_tokens: Arc>>, pub admin_users: Arc>>, diff --git a/crates/core/database/src/models/admin_audits/models.rs b/crates/core/database/src/models/admin_audits/models.rs index 67f5045a..accc10cc 100644 --- a/crates/core/database/src/models/admin_audits/models.rs +++ b/crates/core/database/src/models/admin_audits/models.rs @@ -1,3 +1,6 @@ +use iso8601_timestamp::Timestamp; +use revolt_models::v0::AdminAuditItemActions; + use crate::util::basic::transform_optional_string; auto_derived! { @@ -7,7 +10,7 @@ auto_derived! { pub id: String, /// The moderator who performed the action pub mod_id: String, - /// The action performed (previously 'permission') + /// The action performed (previously 'permission'). Should be one of v0::AdminAuditActionAction pub action: String, /// The relevant case ID, if applicable pub case_id: Option, @@ -15,13 +18,15 @@ auto_derived! { pub target_id: Option, /// The context of the action, if applicable (eg. search phrases) pub context: Option, + /// When the action occurred, in iso8601. + pub timestamp: String } } impl AdminAuditItem { pub fn new( mod_id: &str, - action: &str, + action: AdminAuditItemActions, case_id: Option<&str>, target_id: Option<&str>, context: Option<&str>, @@ -34,6 +39,7 @@ impl AdminAuditItem { case_id: transform_optional_string(case_id), target_id: transform_optional_string(target_id), context: transform_optional_string(context), + timestamp: Timestamp::now_utc().format_short().to_string(), } } } diff --git a/crates/core/database/src/models/admin_case_comments/models.rs b/crates/core/database/src/models/admin_case_comments/models.rs deleted file mode 100644 index 0429d9ce..00000000 --- a/crates/core/database/src/models/admin_case_comments/models.rs +++ /dev/null @@ -1,54 +0,0 @@ -auto_derived_partial! { - pub struct AdminCaseComment { - /// The comment ID - #[serde(rename = "_id")] - pub id: String, - /// The ID of the case this comment is attached to - pub case_id: String, - /// The author ID - pub user_id: String, - /// When the comment was edited, if applicable, in iso8601 - pub edited_at: Option, - /// The content - pub content: String - }, - "PartialAdminCaseComment" -} - -impl AdminCaseComment { - pub fn new(case_id: &str, user_id: &str, content: &str) -> AdminCaseComment { - let id = ulid::Ulid::new().to_string(); - AdminCaseComment { - id, - case_id: case_id.to_string(), - user_id: user_id.to_string(), - edited_at: None, - content: content.to_string(), - } - } - - /// Edit the comment, updating the edited_at time as well - pub fn edit(&mut self, content: &str) { - self.content = content.to_string(); - self.edited_at = Some( - iso8601_timestamp::Timestamp::now_utc() - .format_short() - .to_string(), - ); - } -} - -impl PartialAdminCaseComment { - pub fn new() -> PartialAdminCaseComment { - PartialAdminCaseComment::default() - } - /// Edit the comment, updating the edited_at time as well - pub fn edit(&mut self, content: &str) { - self.content = Some(content.to_string()); - self.edited_at = Some( - iso8601_timestamp::Timestamp::now_utc() - .format_short() - .to_string(), - ); - } -} diff --git a/crates/core/database/src/models/admin_case_comments/ops.rs b/crates/core/database/src/models/admin_case_comments/ops.rs deleted file mode 100644 index 7f555a7d..00000000 --- a/crates/core/database/src/models/admin_case_comments/ops.rs +++ /dev/null @@ -1,19 +0,0 @@ -mod mongodb; -mod reference; - -use revolt_result::Result; - -use crate::{models::admin_case_comments::AdminCaseComment, PartialAdminCaseComment}; - -#[async_trait] -pub trait AbstractAdminCaseComments: Sync + Send { - async fn admin_case_comment_insert(&self, comment: AdminCaseComment) -> Result<()>; - - async fn admin_case_comment_update( - &self, - comment_id: &str, - partial: &PartialAdminCaseComment, - ) -> Result<()>; - - async fn admin_case_comment_fetch(&self, case_id: &str) -> Result>; -} diff --git a/crates/core/database/src/models/admin_case_comments/ops/mongodb.rs b/crates/core/database/src/models/admin_case_comments/ops/mongodb.rs deleted file mode 100644 index b01c0082..00000000 --- a/crates/core/database/src/models/admin_case_comments/ops/mongodb.rs +++ /dev/null @@ -1,36 +0,0 @@ -use revolt_result::Result; - -use crate::MongoDb; -use crate::{AdminCaseComment, PartialAdminCaseComment}; - -use super::AbstractAdminCaseComments; - -static COL: &str = "admin_case_comments"; - -#[async_trait] -impl AbstractAdminCaseComments for MongoDb { - async fn admin_case_comment_insert(&self, comment: AdminCaseComment) -> Result<()> { - query!(self, insert_one, COL, comment).map(|_| ()) - } - - async fn admin_case_comment_update( - &self, - comment_id: &str, - partial: &PartialAdminCaseComment, - ) -> Result<()> { - query!( - self, - update_one_by_id, - COL, - comment_id, - partial, - vec![], - None - ) - .map(|_| ()) - } - - async fn admin_case_comment_fetch(&self, case_id: &str) -> Result> { - query!(self, find, COL, doc! {"case_id": case_id}) - } -} diff --git a/crates/core/database/src/models/admin_case_comments/ops/reference.rs b/crates/core/database/src/models/admin_case_comments/ops/reference.rs deleted file mode 100644 index 25570edf..00000000 --- a/crates/core/database/src/models/admin_case_comments/ops/reference.rs +++ /dev/null @@ -1,49 +0,0 @@ -use iso8601_timestamp::Timestamp; -use revolt_result::Result; - -use crate::ReferenceDb; -use crate::{AdminCaseComment, PartialAdminCaseComment}; - -use super::AbstractAdminCaseComments; - -#[async_trait] -impl AbstractAdminCaseComments for ReferenceDb { - async fn admin_case_comment_insert(&self, comment: AdminCaseComment) -> Result<()> { - let mut admin_case_comments = self.admin_case_comments.lock().await; - if admin_case_comments.contains_key(&comment.id) { - Err(create_database_error!("insert", "admin_case_comments")) - } else { - admin_case_comments.insert(comment.id.to_string(), comment.clone()); - Ok(()) - } - } - - async fn admin_case_comment_update( - &self, - comment_id: &str, - partial: &PartialAdminCaseComment, - ) -> Result<()> { - let mut admin_case_comments = self.admin_case_comments.lock().await; - if let Some(comment) = admin_case_comments.get_mut(comment_id) { - comment.apply_options(partial.clone()); - comment.edited_at = Some(Timestamp::now_utc().format().to_string()); - Ok(()) - } else { - Err(create_error!(NotFound)) - } - } - - async fn admin_case_comment_fetch(&self, case_id: &str) -> Result> { - let admin_case_comments = self.admin_case_comments.lock().await; - Ok(admin_case_comments - .iter() - .filter_map(|(_, c)| { - if c.case_id == case_id { - Some(c.clone()) - } else { - None - } - }) - .collect()) - } -} diff --git a/crates/core/database/src/models/admin_cases/ops.rs b/crates/core/database/src/models/admin_cases/ops.rs index b13a062b..1b3f0039 100644 --- a/crates/core/database/src/models/admin_cases/ops.rs +++ b/crates/core/database/src/models/admin_cases/ops.rs @@ -14,6 +14,8 @@ pub trait AbstractAdminCases: Sync + Send { async fn admin_case_fetch(&self, case_id: &str) -> Result; + async fn admin_case_fetch_from_shorthand(&self, short_id: &str) -> Result; + /// title is fuzzy, the rest of the arguments are direct matches /// before_id and limit are for paginating async fn admin_case_search( diff --git a/crates/core/database/src/models/admin_cases/ops/mongodb.rs b/crates/core/database/src/models/admin_cases/ops/mongodb.rs index f2aa3ac7..f0f38757 100644 --- a/crates/core/database/src/models/admin_cases/ops/mongodb.rs +++ b/crates/core/database/src/models/admin_cases/ops/mongodb.rs @@ -35,6 +35,11 @@ impl AbstractAdminCases for MongoDb { .ok_or_else(|| create_database_error!("find_one", COL)) } + async fn admin_case_fetch_from_shorthand(&self, short_id: &str) -> Result { + query!(self, find_one, COL, doc! {"short_id": short_id})? + .ok_or_else(|| create_database_error!("find_one", COL)) + } + /// title is fuzzy, the rest of the arguments are direct matches async fn admin_case_search( &self, diff --git a/crates/core/database/src/models/admin_cases/ops/reference.rs b/crates/core/database/src/models/admin_cases/ops/reference.rs index a0d4657e..5a1b5491 100644 --- a/crates/core/database/src/models/admin_cases/ops/reference.rs +++ b/crates/core/database/src/models/admin_cases/ops/reference.rs @@ -46,6 +46,15 @@ impl AbstractAdminCases for ReferenceDb { } } + async fn admin_case_fetch_from_shorthand(&self, short_id: &str) -> Result { + let admin_cases = self.admin_cases.lock().await; + if let Some((_, case)) = admin_cases.iter().find(|(_, c)| c.short_id == short_id) { + Ok(case.clone()) + } else { + Err(create_error!(NotFound)) + } + } + /// title is fuzzy, the rest of the arguments are direct matches /// before_id and limit are for paginating async fn admin_case_search( diff --git a/crates/core/database/src/models/admin_case_comments/mod.rs b/crates/core/database/src/models/admin_comments/mod.rs similarity index 100% rename from crates/core/database/src/models/admin_case_comments/mod.rs rename to crates/core/database/src/models/admin_comments/mod.rs diff --git a/crates/core/database/src/models/admin_comments/models.rs b/crates/core/database/src/models/admin_comments/models.rs new file mode 100644 index 00000000..42658c1d --- /dev/null +++ b/crates/core/database/src/models/admin_comments/models.rs @@ -0,0 +1,95 @@ +use revolt_models::v0; + +auto_derived_partial! { + pub struct AdminComment { + /// The comment ID + #[serde(rename = "_id")] + pub id: String, + /// The Object this comment is attached to + pub object_id: String, + /// The ID of the case this comment is attached to, if applicable + pub case_id: Option, + /// The author ID + pub user_id: String, + /// When the comment was edited, if applicable, in iso8601 + pub edited_at: Option, + /// The content, if not a system event + pub content: Option, + /// The system event that occurred, if applicable + pub system_message: Option, + /// The system message target + pub system_message_target: Option, + /// The system message raw context + pub system_message_context: Option, + }, + "PartialAdminComment" +} + +impl AdminComment { + pub fn new( + object_id: &str, + user_id: &str, + content: &str, + case_id: Option<&str>, + ) -> AdminComment { + let id = ulid::Ulid::new().to_string(); + AdminComment { + id, + object_id: object_id.to_string(), + case_id: case_id.map(|c| c.to_string()), + user_id: user_id.to_string(), + edited_at: None, + content: Some(content.to_string()), + system_message: None, + system_message_context: None, + system_message_target: None, + } + } + + pub fn new_system_message( + object_id: &str, + user_id: &str, + case_id: &str, + system_message_kind: v0::AdminAuditItemActions, + context: Option<&str>, + target: &str, + ) -> AdminComment { + let id = ulid::Ulid::new().to_string(); + AdminComment { + id, + object_id: object_id.to_string(), + case_id: Some(case_id.to_string()), + user_id: user_id.to_string(), + edited_at: None, + content: None, + system_message: Some(serde_json::to_string(&system_message_kind).unwrap()), // if this explodes i'll eat my hat. + system_message_context: context.map(|c| c.to_string()), + system_message_target: Some(target.to_string()), + } + } + + /// Edit the comment, updating the edited_at time as well + pub fn edit(&mut self, content: &str) { + self.content = Some(content.to_string()); + self.edited_at = Some( + iso8601_timestamp::Timestamp::now_utc() + .format_short() + .to_string(), + ); + } +} + +impl PartialAdminComment { + pub fn new() -> PartialAdminComment { + PartialAdminComment::default() + } + /// Edit the comment, updating the edited_at time as well + pub fn edit(&mut self, content: &str) { + self.content = Some(content.to_string()); + self.edited_at = Some( + iso8601_timestamp::Timestamp::now_utc() + .format_short() + .to_string(), + ); + } +} diff --git a/crates/core/database/src/models/admin_comments/ops.rs b/crates/core/database/src/models/admin_comments/ops.rs new file mode 100644 index 00000000..524ae5b1 --- /dev/null +++ b/crates/core/database/src/models/admin_comments/ops.rs @@ -0,0 +1,28 @@ +mod mongodb; +mod reference; + +use revolt_result::Result; + +use crate::{models::admin_comments::AdminComment, PartialAdminComment}; + +#[async_trait] +pub trait AbstractAdminComments: Sync + Send { + async fn admin_comment_insert(&self, comment: AdminComment) -> Result<()>; + + async fn admin_comment_update( + &self, + comment_id: &str, + partial: &PartialAdminComment, + ) -> Result<()>; + + async fn admin_comment_fetch(&self, id: &str) -> Result; + + /// Fetch all comments related to the case. This includes comments made on other objects. + async fn admin_comment_fetch_related_case(&self, case_id: &str) -> Result>; + + /// Fetch all comments on an object + async fn admin_comment_fetch_object_comments( + &self, + object_id: &str, + ) -> Result>; +} diff --git a/crates/core/database/src/models/admin_comments/ops/mongodb.rs b/crates/core/database/src/models/admin_comments/ops/mongodb.rs new file mode 100644 index 00000000..a253a0a9 --- /dev/null +++ b/crates/core/database/src/models/admin_comments/ops/mongodb.rs @@ -0,0 +1,47 @@ +use revolt_result::Result; + +use crate::MongoDb; +use crate::{AdminComment, PartialAdminComment}; + +use super::AbstractAdminComments; + +static COL: &str = "admin_comments"; + +#[async_trait] +impl AbstractAdminComments for MongoDb { + async fn admin_comment_insert(&self, comment: AdminComment) -> Result<()> { + query!(self, insert_one, COL, comment).map(|_| ()) + } + + async fn admin_comment_update( + &self, + comment_id: &str, + partial: &PartialAdminComment, + ) -> Result<()> { + query!( + self, + update_one_by_id, + COL, + comment_id, + partial, + vec![], + None + ) + .map(|_| ()) + } + + async fn admin_comment_fetch(&self, id: &str) -> Result { + query!(self, find_one, COL, doc! {"_id": id})?.ok_or_else(|| create_error!(NotFound)) + } + + async fn admin_comment_fetch_related_case(&self, case_id: &str) -> Result> { + query!(self, find, COL, doc! {"case_id": case_id}) + } + + async fn admin_comment_fetch_object_comments( + &self, + object_id: &str, + ) -> Result> { + query!(self, find, COL, doc! {"object_id": object_id}) + } +} diff --git a/crates/core/database/src/models/admin_comments/ops/reference.rs b/crates/core/database/src/models/admin_comments/ops/reference.rs new file mode 100644 index 00000000..f3b81331 --- /dev/null +++ b/crates/core/database/src/models/admin_comments/ops/reference.rs @@ -0,0 +1,73 @@ +use iso8601_timestamp::Timestamp; +use revolt_result::Result; + +use crate::ReferenceDb; +use crate::{AdminComment, PartialAdminComment}; + +use super::AbstractAdminComments; + +#[async_trait] +impl AbstractAdminComments for ReferenceDb { + async fn admin_comment_insert(&self, comment: AdminComment) -> Result<()> { + let mut admin_comments = self.admin_comments.lock().await; + if admin_comments.contains_key(&comment.id) { + Err(create_database_error!("insert", "admin_comments")) + } else { + admin_comments.insert(comment.id.to_string(), comment.clone()); + Ok(()) + } + } + + async fn admin_comment_update( + &self, + comment_id: &str, + partial: &PartialAdminComment, + ) -> Result<()> { + let mut admin_comments = self.admin_comments.lock().await; + if let Some(comment) = admin_comments.get_mut(comment_id) { + comment.apply_options(partial.clone()); + comment.edited_at = Some(Timestamp::now_utc().format().to_string()); + Ok(()) + } else { + Err(create_error!(NotFound)) + } + } + + async fn admin_comment_fetch(&self, id: &str) -> Result { + let admin_comments = self.admin_comments.lock().await; + admin_comments + .get(id) + .map_or(Err(create_error!(NotFound)), |ac| Ok(ac.clone())) + } + + async fn admin_comment_fetch_related_case(&self, case_id: &str) -> Result> { + let admin_comments = self.admin_comments.lock().await; + Ok(admin_comments + .iter() + .filter_map(|(_, c)| { + if c.case_id.as_ref().is_some_and(|c| c == case_id) { + Some(c.clone()) + } else { + None + } + }) + .collect()) + } + + async fn admin_comment_fetch_object_comments( + &self, + object_id: &str, + ) -> Result> { + let admin_comments = self.admin_comments.lock().await; + Ok(admin_comments + .iter() + .filter_map(|(_, c)| { + if c.object_id == object_id { + Some(c.clone()) + } else { + None + } + }) + .collect()) + } +} diff --git a/crates/core/database/src/models/admin_migrations/ops/mongodb.rs b/crates/core/database/src/models/admin_migrations/ops/mongodb.rs index 703e378a..7db525f0 100644 --- a/crates/core/database/src/models/admin_migrations/ops/mongodb.rs +++ b/crates/core/database/src/models/admin_migrations/ops/mongodb.rs @@ -28,6 +28,20 @@ impl AbstractMigrations for MongoDb { init::create_database(self).await; } + let config = revolt_config::config().await; + + if config.features.admin_api_enabled { + let db = self.db(); + let colls = db + .list_collection_names() + .await + .expect("Failed to fetch collection names."); + if !colls.iter().any(|x| x == "admin_audits") { + info!("You've enabled the admin api for the first time. Setting up database..."); + init::create_admin_database(&db).await; + } + } + Ok(()) } } diff --git a/crates/core/database/src/models/admin_migrations/ops/mongodb/init.rs b/crates/core/database/src/models/admin_migrations/ops/mongodb/init.rs index 4a422cbb..d20a5c02 100644 --- a/crates/core/database/src/models/admin_migrations/ops/mongodb/init.rs +++ b/crates/core/database/src/models/admin_migrations/ops/mongodb/init.rs @@ -8,6 +8,8 @@ pub async fn create_database(db: &MongoDb) { info!("Creating database."); let db = db.db(); + let config = revolt_config::config().await; + db.create_collection("accounts") .await .expect("Failed to create accounts collection."); @@ -263,5 +265,135 @@ pub async fn create_database(db: &MongoDb) { .await .expect("Failed to create ratelimit_events index."); + // Only create these tables if the admin api is enabled + if config.features.admin_api_enabled { + create_admin_database(&db).await; + } + info!("Created database."); } + +pub async fn create_admin_database(db: &mongodb::Database) { + db.create_collection("admin_audits") + .await + .expect("Failed to create admin_audits collection."); + + db.create_collection("admin_comments") + .await + .expect("Failed to create admin_comments collection."); + + db.create_collection("admin_cases") + .await + .expect("Failed to create admin_cases collection."); + + db.create_collection("admin_strikes") + .await + .expect("Failed to create admin_strikes collection."); + + db.create_collection("admin_tokens") + .await + .expect("Failed to create admin_tokens collection."); + + db.create_collection("admin_users") + .await + .expect("Failed to create admin_users collection."); + + db.run_command(doc! { + "createIndexes": "admin_comments", + "indexes": [ + { + "key": { + "case_id": 1_i32, + }, + "unique": false, + "name": "case_id" + }, + { + "key": { + "object_id": 1_i32, + }, + "unique": false, + "name": "object_id" + } + ] + }) + .await + .expect("Failed to create admin_comments index."); + + db.run_command(doc! { + "createIndexes": "admin_cases", + "indexes": [ + { + "key": { + "id": 1_i32, + }, + "unique": true, + "name": "id" + }, + { + "key": { + "short_id": 1_i32, + }, + "unique": true, + "name": "short_id" + } + ] + }) + .await + .expect("Failed to create admin_cases index."); + + db.run_command(doc! { + "createIndexes": "admin_users", + "indexes": [ + { + "key": { + "email": 1_i32, + }, + "unique": true, + "name": "case_id" + }, + ] + }) + .await + .expect("Failed to create admin_comments index."); + + db.run_command(doc! { + "createIndexes": "admin_audits", + "indexes": [ + { + "key": { + "mod_id": 1_i32, + }, + "unique": false, + "name": "mod_id" + }, + { + "key": { + "target_id": 1_i32, + }, + "unique": false, + "name": "target_id" + } + ] + }) + .await + .expect("Failed to create admin_audits index."); + + // indexes on regular data that are only needed for admin queries + db.run_command(doc! { + "createIndexes": "server_members", + "indexes": [ + { + "key": { + "joined_at": 1_i32, + }, + "unique": false, + "name": "joined_at" + }, + ] + }) + .await + .expect("Failed to create server_members::joined_at index."); + + info!("Created admin database."); +} diff --git a/crates/core/database/src/models/admin_notes/mod.rs b/crates/core/database/src/models/admin_notes/mod.rs deleted file mode 100644 index ead17df0..00000000 --- a/crates/core/database/src/models/admin_notes/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod models; -mod ops; - -pub use models::*; -pub use ops::*; diff --git a/crates/core/database/src/models/admin_notes/models.rs b/crates/core/database/src/models/admin_notes/models.rs deleted file mode 100644 index f4be8229..00000000 --- a/crates/core/database/src/models/admin_notes/models.rs +++ /dev/null @@ -1,35 +0,0 @@ -auto_derived_partial! { - pub struct AdminObjectNote { - /// The ID of the note, which is the same as the ID of the object it's attached to - #[serde(rename = "_id")] - pub id: String, - /// When the note was edited, in iso8601 - pub edited_at: String, - /// The last user to edit the note - pub last_edited_by_id: String, - /// The content of the note - pub content: String, - }, - "PartialAdminObjectNote" -} - -impl AdminObjectNote { - pub fn new(object_id: &str, user_id: &str, content: &str) -> AdminObjectNote { - AdminObjectNote { - id: object_id.to_string(), - edited_at: iso8601_timestamp::Timestamp::now_utc() - .format_short() - .to_string(), - last_edited_by_id: user_id.to_string(), - content: content.to_string(), - } - } - pub fn to_partial(&self) -> PartialAdminObjectNote { - PartialAdminObjectNote { - id: Some(self.id.clone()), - edited_at: Some(self.edited_at.clone()), - last_edited_by_id: Some(self.last_edited_by_id.clone()), - content: Some(self.content.clone()), - } - } -} diff --git a/crates/core/database/src/models/admin_notes/ops.rs b/crates/core/database/src/models/admin_notes/ops.rs deleted file mode 100644 index d45da92f..00000000 --- a/crates/core/database/src/models/admin_notes/ops.rs +++ /dev/null @@ -1,12 +0,0 @@ -mod mongodb; -mod reference; -use revolt_result::Result; - -use crate::models::admin_notes::AdminObjectNote; - -#[async_trait] -pub trait AbstractAdminNotes: Sync + Send { - async fn admin_note_update(&self, note: AdminObjectNote) -> Result<()>; - - async fn admin_note_fetch(&self, target_id: &str) -> Result; -} diff --git a/crates/core/database/src/models/admin_notes/ops/mongodb.rs b/crates/core/database/src/models/admin_notes/ops/mongodb.rs deleted file mode 100644 index f34ff664..00000000 --- a/crates/core/database/src/models/admin_notes/ops/mongodb.rs +++ /dev/null @@ -1,34 +0,0 @@ -use revolt_result::Result; - -use crate::AdminObjectNote; -use crate::MongoDb; - -use super::AbstractAdminNotes; - -static COL: &str = "admin_notes"; - -#[async_trait] -impl AbstractAdminNotes for MongoDb { - async fn admin_note_update(&self, note: AdminObjectNote) -> Result<()> { - let resp: Result<()> = query!(self, insert_one, COL, note.clone()).map(|_| ()); - if resp.is_err() { - query!( - self, - update_one_by_id, - COL, - note.id.as_str(), - note.to_partial(), - vec![], - None - ) - .map(|_| ()) - } else { - Ok(()) - } - } - - async fn admin_note_fetch(&self, target_id: &str) -> Result { - query!(self, find_one_by_id, COL, target_id)? - .ok_or_else(|| create_database_error!("find_one", COL)) - } -} diff --git a/crates/core/database/src/models/admin_notes/ops/reference.rs b/crates/core/database/src/models/admin_notes/ops/reference.rs deleted file mode 100644 index 370b3744..00000000 --- a/crates/core/database/src/models/admin_notes/ops/reference.rs +++ /dev/null @@ -1,29 +0,0 @@ -use revolt_result::Result; - -use crate::AdminObjectNote; -use crate::ReferenceDb; - -use super::AbstractAdminNotes; - -#[async_trait] -impl AbstractAdminNotes for ReferenceDb { - async fn admin_note_update(&self, note: AdminObjectNote) -> Result<()> { - let mut admin_notes = self.admin_object_notes.lock().await; - if let Some(existing_note) = admin_notes.get_mut(¬e.id) { - existing_note.apply_options(note.to_partial()); - Ok(()) - } else { - admin_notes.insert(note.id.clone(), note); - Ok(()) - } - } - - async fn admin_note_fetch(&self, target_id: &str) -> Result { - let admin_notes = self.admin_object_notes.lock().await; - if let Some(note) = admin_notes.get(target_id) { - Ok(note.clone()) - } else { - Err(create_error!(NotFound)) - } - } -} diff --git a/crates/core/database/src/models/channel_invites/ops/mongodb.rs b/crates/core/database/src/models/channel_invites/ops/mongodb.rs index d628be8e..5d1f32ce 100644 --- a/crates/core/database/src/models/channel_invites/ops/mongodb.rs +++ b/crates/core/database/src/models/channel_invites/ops/mongodb.rs @@ -12,7 +12,14 @@ static COL: &str = "channel_invites"; impl AbstractChannelInvites for MongoDb { /// Insert a new invite into the database async fn insert_invite(&self, invite: &Invite) -> Result<()> { - query!(self, insert_one, COL, &invite).map(|_| ()) + self.insert_one(COL, &invite).await.or_else(|e| { + if e.to_string().contains("duplicate") { + return Err(create_error!(InviteExists)); + } else { + return Err(create_database_error!("insert_one", COL)); + } + })?; + Ok(()) } /// Fetch an invite by the code diff --git a/crates/core/database/src/models/mod.rs b/crates/core/database/src/models/mod.rs index af990d57..8eacf8c9 100644 --- a/crates/core/database/src/models/mod.rs +++ b/crates/core/database/src/models/mod.rs @@ -1,8 +1,7 @@ mod admin_audits; -mod admin_case_comments; mod admin_cases; +mod admin_comments; mod admin_migrations; -mod admin_notes; mod admin_strikes; mod admin_tokens; mod admin_users; @@ -26,10 +25,9 @@ mod user_settings; mod users; pub use admin_audits::*; -pub use admin_case_comments::*; pub use admin_cases::*; +pub use admin_comments::*; pub use admin_migrations::*; -pub use admin_notes::*; pub use admin_strikes::*; pub use admin_tokens::*; pub use admin_users::*; @@ -58,9 +56,8 @@ pub trait AbstractDatabase: Sync + Send + admin_audits::AbstractAdminAudits - + admin_case_comments::AbstractAdminCaseComments + admin_cases::AbstractAdminCases - + admin_notes::AbstractAdminNotes + + admin_comments::AbstractAdminComments + admin_strikes::AbstractAdminStrikes + admin_users::AbstractAdminUsers + admin_tokens::AbstractAdminTokens diff --git a/crates/core/database/src/models/server_members/model.rs b/crates/core/database/src/models/server_members/model.rs index 25b6979a..d1d51c19 100644 --- a/crates/core/database/src/models/server_members/model.rs +++ b/crates/core/database/src/models/server_members/model.rs @@ -81,6 +81,7 @@ impl Member { server: &Server, user: &User, channels: Option>, + create_alert: bool, ) -> Result<(Member, Vec)> { if db.fetch_ban(&server.id, &user.id).await.is_ok() { return Err(create_error!(Banned)); @@ -141,18 +142,20 @@ impl Member { .private(user.id.clone()) .await; - if let Some(id) = server - .system_messages - .as_ref() - .and_then(|x| x.user_joined.as_ref()) - { - SystemMessage::UserJoined { - id: user.id.clone(), + if create_alert { + if let Some(id) = server + .system_messages + .as_ref() + .and_then(|x| x.user_joined.as_ref()) + { + SystemMessage::UserJoined { + id: user.id.clone(), + } + .into_message(id.to_string()) + .send_without_notifications(db, None, None, false, false, false) + .await + .ok(); } - .into_message(id.to_string()) - .send_without_notifications(db, None, None, false, false, false) - .await - .ok(); } Ok((member, channels)) diff --git a/crates/core/database/src/models/server_members/ops.rs b/crates/core/database/src/models/server_members/ops.rs index 1d5a045f..b3121fcf 100644 --- a/crates/core/database/src/models/server_members/ops.rs +++ b/crates/core/database/src/models/server_members/ops.rs @@ -117,4 +117,18 @@ pub trait AbstractServerMembers: Sync + Send { /// Delete a server member by their id async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()>; + + /// Fetch all participants of a server. + async fn fetch_server_participants( + &self, + server_id: &str, + ) -> Result)>>; + + /// Fetch all members of a server + async fn fetch_server_members( + &self, + server_id: &str, + page_size: u8, + after: Option, + ) -> Result>; } diff --git a/crates/core/database/src/models/server_members/ops/mongodb.rs b/crates/core/database/src/models/server_members/ops/mongodb.rs index 07d8a819..6437591f 100644 --- a/crates/core/database/src/models/server_members/ops/mongodb.rs +++ b/crates/core/database/src/models/server_members/ops/mongodb.rs @@ -1,8 +1,10 @@ +use std::collections::HashMap; + use futures::StreamExt; use mongodb::options::ReadConcern; use revolt_result::Result; -use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember}; +use crate::{AbstractUsers, FieldsMember, Member, MemberCompositeKey, PartialMember}; use crate::{IntoDocumentPath, MongoDb}; use super::{AbstractServerMembers, ChunkedServerMembersGenerator}; @@ -238,6 +240,88 @@ impl AbstractServerMembers for MongoDb { ) .map(|_| ()) } + + async fn fetch_server_participants( + &self, + server_id: &str, + ) -> Result)>> { + let server: crate::Server = query!(self, find_one, "servers", doc! {"_id": server_id})? + .ok_or_else(|| create_database_error!("find", "servers"))?; + let ids: Vec = self + .db() + .collection::("messages") + .distinct("author", doc! {"channel": {"$in": server.channels}}) + .await + .map_err(|_| create_database_error!("distinct", "messages"))? + .iter() + .map(|b| bson::from_bson::(b.clone()).unwrap()) // json encoded string for some logic-defying reason + .collect(); + + println!("{:?}", &ids); + + let users = self.fetch_users(&ids).await?; + let members = self + .fetch_members(server_id, &ids) + .await? + .iter() + .map(|f| (f.id.user.clone(), f.clone())) + .collect::>(); + + Ok(users + .iter() + .map(|u| (u.clone(), members.get(&u.id).cloned())) + .collect()) + } + + async fn fetch_server_members( + &self, + server_id: &str, + page_size: u8, + after: Option, + ) -> Result> { + let mut members_stream = if let Some(after) = after { + self.col::(COL) + .find(doc! {"_id.server": server_id, "joined_at": {"$gt": after as i64}}) + .sort(doc! {"joined_at": -1}) + .limit(page_size as i64) + .await + .map_err(|_| create_database_error!("find", COL))? + } else { + self.col::(COL) + .find(doc! {"_id.server": server_id}) + .sort(doc! {"joined_at": -1}) + .limit(page_size as i64) + .await + .map_err(|_| create_database_error!("find", COL))? + }; + + let mut members = vec![]; + if members_stream.advance().await.is_ok_and(|f| f) { + loop { + if let Ok(x) = members_stream.deserialize_current() { + members.push(x); + if let Ok(r) = members_stream.advance().await { + if !r { + break; + } + } else { + break; + } + } + } + } + + let user_ids: Vec = members.iter().map(|m| m.id.user.clone()).collect(); + let mut set = HashMap::new(); + self.fetch_users(&user_ids).await?.iter().for_each(|f| { + set.insert(f.id.clone(), f.clone()); + }); + + return Ok(members + .iter() + .map(|f| (set.remove(&f.id.user).unwrap(), f.clone())) + .collect()); + } } impl IntoDocumentPath for FieldsMember { diff --git a/crates/core/database/src/models/server_members/ops/reference.rs b/crates/core/database/src/models/server_members/ops/reference.rs index ac15d977..a13b9108 100644 --- a/crates/core/database/src/models/server_members/ops/reference.rs +++ b/crates/core/database/src/models/server_members/ops/reference.rs @@ -1,3 +1,6 @@ +use std::collections::HashSet; + +use iso8601_timestamp::Timestamp; use revolt_result::Result; use crate::ReferenceDb; @@ -178,4 +181,91 @@ impl AbstractServerMembers for ReferenceDb { Err(create_error!(NotFound)) } } + + async fn fetch_server_participants( + &self, + server_id: &str, + ) -> Result)>> { + let servers = self.servers.lock().await; + let server = servers.get(server_id).ok_or(create_error!(NotFound))?; + + let mut hash = HashSet::new(); + server.channels.iter().for_each(|c| { + hash.insert(c.clone()); + }); + + drop(servers); + + let messages = self.messages.lock().await; + let userids = messages + .iter() + .filter(|(_, message)| hash.contains(&message.channel)) + .map(|(_, message)| message.author.clone()) + .collect::>(); + + drop(messages); + + let users = self.users.lock().await; + let members = self.server_members.lock().await; + + let resp_users: Vec<(crate::User, Option)> = users + .iter() + .filter(|(uid, _)| userids.contains(*uid)) + .map(|(uid, user)| { + let key = MemberCompositeKey { + server: server_id.to_string(), + user: uid.clone(), + }; + // fuck it we ball + (user.clone(), members.get(&key).map(|f| f.clone())) + }) + .collect(); + + Ok(resp_users) + } + + /// Fetch all members of a server + async fn fetch_server_members( + &self, + server_id: &str, + page_size: u8, + after: Option, + ) -> Result> { + let members = self.server_members.lock().await; + let users = self.users.lock().await; + + let mut server_members: Vec<&Member> = members + .iter() + .filter(|(_, f)| f.id.server == server_id) + .map(|(_, m)| m) + .collect(); + + server_members.sort_by(|a, b| a.joined_at.cmp(&b.joined_at)); + let iterator = server_members.iter(); + + let mut resp: Vec<&&Member> = if let Some(after) = after { + iterator + .skip_while(|f| { + f.joined_at + .duration_since(Timestamp::UNIX_EPOCH) + .whole_milliseconds() + <= after as i128 + }) + .collect() + } else { + iterator.collect() + }; + + resp.truncate(page_size as usize); + + Ok(resp + .iter() + .map(|f| { + ( + users.get(&f.id.user).unwrap().clone(), + f.clone().clone().clone(), + ) + }) + .collect()) + } } diff --git a/crates/core/database/src/util/bridge/v0.rs b/crates/core/database/src/util/bridge/v0.rs index d0cd5521..bbf8d03d 100644 --- a/crates/core/database/src/util/bridge/v0.rs +++ b/crates/core/database/src/util/bridge/v0.rs @@ -1447,12 +1447,38 @@ impl From for AdminToken { } } -impl From for AdminObjectNote { - fn from(value: crate::AdminObjectNote) -> Self { - AdminObjectNote { - edited_at: Timestamp::parse(&value.edited_at).unwrap(), // if it's invalid it shouldn't have made it to this point. - last_edited_by_id: value.last_edited_by_id, +impl From for AdminComment { + fn from(value: crate::AdminComment) -> Self { + AdminComment { + id: value.id, + case_id: value.case_id, + object_id: value.object_id, + user_id: value.user_id, + edited_at: value.edited_at.map(|f| Timestamp::parse(&f).unwrap()), content: value.content, + system_message: value + .system_message + .map(|f| serde_json::from_str(&f).unwrap()), + system_message_target: value.system_message_target, + system_message_context: value.system_message_context, + } + } +} + +impl From for crate::AdminComment { + fn from(value: AdminComment) -> Self { + crate::AdminComment { + id: value.id, + object_id: value.object_id, + case_id: value.case_id, + user_id: value.user_id, + edited_at: value.edited_at.map(|f| f.format_short().to_string()), + content: value.content, + system_message: value + .system_message + .map(|f| serde_json::to_string(&f).unwrap()), // i'll eat my hat if this fails + system_message_target: value.system_message_target, + system_message_context: value.system_message_context, } } } diff --git a/crates/core/database/src/util/reference.rs b/crates/core/database/src/util/reference.rs index f3d4537d..5d2a9152 100644 --- a/crates/core/database/src/util/reference.rs +++ b/crates/core/database/src/util/reference.rs @@ -2,7 +2,7 @@ use std::str::FromStr; use revolt_result::Result; #[cfg(feature = "rocket-impl")] -use rocket::request::FromParam; +use rocket::{form::FromFormField, request::FromParam}; #[cfg(feature = "rocket-impl")] use schemars::{ schema::{InstanceType, Schema, SchemaObject, SingleOrVec}, @@ -133,3 +133,10 @@ impl JsonSchema for Reference { }) } } + +#[cfg(feature = "rocket-impl")] +impl<'r> FromFormField<'r> for Reference { + fn from_value(field: rocket::form::ValueField<'r>) -> rocket::form::Result<'r, Self> { + Ok(Reference::from_unchecked(field.value.into())) + } +} diff --git a/crates/core/models/src/v0/admin.rs b/crates/core/models/src/v0/admin.rs index ed8a6a07..c9c9c98a 100644 --- a/crates/core/models/src/v0/admin.rs +++ b/crates/core/models/src/v0/admin.rs @@ -1,6 +1,6 @@ use iso8601_timestamp::Timestamp; -use crate::v0::{Report, User}; +use crate::v0::{self, Report, User}; auto_derived! { #[derive(Default)] @@ -28,7 +28,7 @@ auto_derived! { #[cfg_attr(feature="serde", serde(rename="mod"))] pub mod_id: String, /// The action performed (previously 'permission') - pub action: String, + pub action: AdminAuditItemActions, /// The relevant case ID, if applicable #[cfg_attr(feature="serde", serde(rename="case"))] pub case_id: String, @@ -37,14 +37,19 @@ auto_derived! { pub target_id: Option, /// The context attached to the action, if applicable (eg. search phrases) pub context: Option, + /// The time the action occurred at. + pub timestamp: Timestamp, } #[derive(Default)] #[cfg_attr(feature = "validator", derive(validator::Validate))] - pub struct AdminCaseCommentCreate { - /// The ID of the case this comment is attached to + pub struct AdminCommentCreate { + /// The ID of the case this comment is attached to, if applicable. This should be the 7 character short code. #[cfg_attr(feature="serde", serde(rename="case"))] - pub case_id: String, + pub case_id: Option, + /// The ID of the object this comment is attached to. Use full case ULID when creating a case comment. + #[cfg_attr(feature="serde", serde(rename="object"))] + pub object_id: String, /// The content #[cfg_attr(feature = "validator", validate(length(min = 1, max = 2000)))] pub content: String @@ -52,25 +57,39 @@ auto_derived! { #[derive(Default)] #[cfg_attr(feature = "validator", derive(validator::Validate))] - pub struct AdminCaseCommentEdit { + pub struct AdminCommentEdit { /// The new content #[cfg_attr(feature = "validator", validate(length(min = 1, max = 2000)))] pub content: String } - pub struct AdminCaseComment { + pub struct AdminComment { /// The comment ID pub id: String, - /// The ID of the case this comment is attached to + /// The ID of the case this comment is attached to, if applicable. This should be the 7 character short code #[cfg_attr(feature="serde", serde(rename="case"))] - pub case_id: String, - /// The user who posted the comment + #[cfg_attr(feature="serde", serde(skip_serializing_if="Option::is_none"))] + pub case_id: Option, + /// The object this comment is attached to. If this is referencing a case this is the full case ID + #[cfg_attr(feature="serde", serde(rename="object"))] + pub object_id: String, + /// The user who posted the comment/action #[cfg_attr(feature="serde", serde(rename="user"))] pub user_id: String, /// When the comment was edited, if applicable, in iso8601 pub edited_at: Option, - /// The content - pub content: String + /// The content, if this is not a system event + #[cfg_attr(feature="serde", serde(skip_serializing_if="Option::is_none"))] + pub content: Option, + /// The system event that happened, if this is not a comment (only applicable for cases) + #[cfg_attr(feature="serde", serde(skip_serializing_if="Option::is_none"))] + pub system_message: Option, + /// The system message target + #[cfg_attr(feature="serde", serde(skip_serializing_if="Option::is_none"))] + pub system_message_target: Option, + /// The system message raw context + #[cfg_attr(feature="serde", serde(skip_serializing_if="Option::is_none"))] + pub system_message_context: Option, } #[derive(Default)] @@ -119,23 +138,6 @@ auto_derived! { pub reports: Vec } - - pub struct AdminObjectNote { - /// When the note was edited, in iso8601 - pub edited_at: Timestamp, - /// The last user to edit the note - #[cfg_attr(feature="serde", serde(rename="last_edited_by"))] - pub last_edited_by_id: String, - /// The content of the note - pub content: String, - } - - #[cfg_attr(feature = "validator", derive(validator::Validate))] - pub struct AdminObjectNoteEdit { - #[cfg_attr(feature = "validator", validate(length(max = 2000)))] - pub content: String - } - pub struct AdminStrike { /// The strike ID pub id: String, @@ -259,9 +261,11 @@ auto_derived! { pub permissions: Option, } - #[repr(u64)] + // Flags + + #[repr(u32)] pub enum AdminUserPermissionFlags { - ObjectNotes = 0, + Comments = 0, ManageAdminUsers = 1, CreateTokens = 2, @@ -285,4 +289,88 @@ auto_derived! { ManageNotes = 15, Discover = 16, } + + pub enum AdminAuditItemActions { + // Meta + CreateAdminUser, + EditAdminUser, + CreateToken, + RevokeToken, + + // Comments + CommentCreate, + CommentEdit, + CommentFetchForObject, + + // Servers + ServerFetch, + ServerFetchParticipants, + ServerFetchMembers, + ServerAddMember, + ServerChangeOwner, + ServerCreateInvite, + ServerDeleteInvite, + ServerDeleteAllInvites, + ServerDelete, + ServerEdit, + ServerRemoveMember, + ServerSetFlags, + ServerInstanceBanAllMembers, + ServerBanMember, + ServerUnbanMember + } + + // Joiner payloads + pub struct AdminServerResponse { + pub server: v0::Server, + pub owner: v0::User, + pub comments: Vec, + } + + pub struct AdminServerParticipantsResponse { + pub users: Vec, + pub members: Vec, + pub sort_strategy: String + } + + pub struct AdminMemberWithUserAndOffsetResponse { + pub after: Option, + pub users: Vec + } +} + +impl AdminAuditItemActions { + /// Does this action create a case comment? + pub fn makes_comment(&self) -> bool { + match self { + AdminAuditItemActions::CreateAdminUser => false, + AdminAuditItemActions::EditAdminUser => false, + AdminAuditItemActions::CreateToken => false, + AdminAuditItemActions::RevokeToken => false, + AdminAuditItemActions::CommentCreate => false, + AdminAuditItemActions::CommentEdit => false, + AdminAuditItemActions::CommentFetchForObject => false, + AdminAuditItemActions::ServerFetch => false, + AdminAuditItemActions::ServerFetchParticipants => false, + AdminAuditItemActions::ServerAddMember => true, + AdminAuditItemActions::ServerChangeOwner => true, + AdminAuditItemActions::ServerCreateInvite => true, + AdminAuditItemActions::ServerDeleteInvite => true, + AdminAuditItemActions::ServerDeleteAllInvites => true, + AdminAuditItemActions::ServerDelete => true, + AdminAuditItemActions::ServerEdit => true, + AdminAuditItemActions::ServerRemoveMember => true, + AdminAuditItemActions::ServerSetFlags => true, + AdminAuditItemActions::ServerInstanceBanAllMembers => true, + AdminAuditItemActions::ServerBanMember => true, + AdminAuditItemActions::ServerUnbanMember => true, + AdminAuditItemActions::ServerFetchMembers => false, + } + } +} + +impl std::fmt::Display for AdminAuditItemActions { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } } diff --git a/crates/core/models/src/v0/server_members.rs b/crates/core/models/src/v0/server_members.rs index 5b26b88a..6ccd8492 100644 --- a/crates/core/models/src/v0/server_members.rs +++ b/crates/core/models/src/v0/server_members.rs @@ -127,4 +127,9 @@ auto_derived!( #[cfg_attr(feature = "validator", validate(length(min = 1)))] pub remove: Option>, } + + pub struct MemberWithUserResponse { + pub user: crate::v0::User, + pub member: Member, + } ); diff --git a/crates/core/result/src/axum.rs b/crates/core/result/src/axum.rs index e9bee77a..70ca594b 100644 --- a/crates/core/result/src/axum.rs +++ b/crates/core/result/src/axum.rs @@ -34,8 +34,10 @@ impl IntoResponse for Error { ErrorType::GroupTooLarge { .. } => StatusCode::FORBIDDEN, ErrorType::AlreadyInGroup => StatusCode::CONFLICT, ErrorType::NotInGroup => StatusCode::NOT_FOUND, + ErrorType::NotAMember => StatusCode::BAD_REQUEST, ErrorType::AlreadyPinned => StatusCode::BAD_REQUEST, ErrorType::NotPinned => StatusCode::BAD_REQUEST, + ErrorType::InviteExists => StatusCode::BAD_REQUEST, ErrorType::UnknownServer => StatusCode::NOT_FOUND, ErrorType::InvalidRole => StatusCode::NOT_FOUND, diff --git a/crates/core/result/src/lib.rs b/crates/core/result/src/lib.rs index 6e58fcab..bc76590b 100644 --- a/crates/core/result/src/lib.rs +++ b/crates/core/result/src/lib.rs @@ -101,11 +101,13 @@ pub enum ErrorType { NotInGroup, AlreadyPinned, NotPinned, + InviteExists, // ? Server related errors UnknownServer, InvalidRole, Banned, + NotAMember, TooManyServers { max: usize, }, diff --git a/crates/core/result/src/rocket.rs b/crates/core/result/src/rocket.rs index 9c0260f0..a14cf3da 100644 --- a/crates/core/result/src/rocket.rs +++ b/crates/core/result/src/rocket.rs @@ -40,9 +40,11 @@ impl<'r> Responder<'r, 'static> for Error { ErrorType::GroupTooLarge { .. } => Status::Forbidden, ErrorType::AlreadyInGroup => Status::Conflict, ErrorType::NotInGroup => Status::NotFound, + ErrorType::NotAMember => Status::BadRequest, ErrorType::AlreadyPinned => Status::BadRequest, ErrorType::NotPinned => Status::BadRequest, ErrorType::InvalidFlagValue => Status::BadRequest, + ErrorType::InviteExists => Status::BadRequest, ErrorType::UnknownServer => Status::NotFound, ErrorType::InvalidRole => Status::NotFound, diff --git a/crates/delta/src/routes/admin/comments/comment_create.rs b/crates/delta/src/routes/admin/comments/comment_create.rs new file mode 100644 index 00000000..d1051f7a --- /dev/null +++ b/crates/delta/src/routes/admin/comments/comment_create.rs @@ -0,0 +1,44 @@ +use revolt_database::{AdminAuthorization, AdminComment, Database}; +use revolt_models::v0::{self}; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; + +use crate::routes::admin::util::{ + create_audit_action, flatten_authorized_user, user_has_permission, +}; + +/// Create a comment on an object or case. Requires Comments permission. +#[openapi(tag = "Admin")] +#[post("/comments", data = "")] +pub async fn admin_comment_create( + db: &State, + auth: AdminAuthorization, + body: Json, +) -> Result> { + let user = flatten_authorized_user(&auth); + if !user_has_permission(user, v0::AdminUserPermissionFlags::Comments) { + return Err(create_error!(MissingPermission { + permission: "Comments".to_string() + })); + } + + let comment = AdminComment::new( + &body.object_id, + &user.id, + &body.content, + body.case_id.as_deref(), + ); + db.admin_comment_insert(comment.clone()).await?; + + create_audit_action( + db, + &user.id, + v0::AdminAuditItemActions::CommentCreate, + None, + Some(&body.object_id), + None, + ) + .await?; + + Ok(Json(comment.into())) +} diff --git a/crates/delta/src/routes/admin/comments/comment_edit.rs b/crates/delta/src/routes/admin/comments/comment_edit.rs new file mode 100644 index 00000000..b6027e36 --- /dev/null +++ b/crates/delta/src/routes/admin/comments/comment_edit.rs @@ -0,0 +1,56 @@ +use iso8601_timestamp::Timestamp; +use revolt_database::{ + util::reference::Reference, AdminAuthorization, Database, PartialAdminComment, +}; +use revolt_models::v0::{self}; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; +use rocket_empty::EmptyResponse; + +use crate::routes::admin::util::{ + create_audit_action, flatten_authorized_user, user_has_permission, +}; + +/// Create a comment on an object or case. Requires Comments permission. +#[openapi(tag = "Admin")] +#[patch("/comments/", data = "")] +pub async fn admin_comment_edit( + db: &State, + auth: AdminAuthorization, + id: Reference, + body: Json, +) -> Result { + let user = flatten_authorized_user(&auth); + if !user_has_permission(user, v0::AdminUserPermissionFlags::Comments) { + return Err(create_error!(MissingPermission { + permission: "Comments".to_string() + })); + } + + let existing = db.admin_comment_fetch(&id.id).await?; + if existing.user_id != user.id { + return Err(create_error!(MissingPermission { + permission: "CannotEditOthersComment".to_string() + })); + } + + let partial = PartialAdminComment { + content: Some(body.content.clone()), + edited_at: Some(Timestamp::now_utc().format_short().to_string()), + ..Default::default() + }; + db.admin_comment_update(&id.id, &partial).await?; + + let context = format!("comment id {:}, object id {:}", &id.id, &existing.object_id); + create_audit_action( + db, + &user.id, + v0::AdminAuditItemActions::CommentEdit, + None, + Some(&existing.object_id), + Some(&context), + ) + .await?; + + Ok(EmptyResponse) +} diff --git a/crates/delta/src/routes/admin/comments/comment_fetch_case.rs b/crates/delta/src/routes/admin/comments/comment_fetch_case.rs new file mode 100644 index 00000000..aeb80474 --- /dev/null +++ b/crates/delta/src/routes/admin/comments/comment_fetch_case.rs @@ -0,0 +1,43 @@ +use revolt_database::{util::reference::Reference, AdminAuthorization, Database}; +use revolt_models::v0::{self}; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; + +use crate::routes::admin::util::{ + create_audit_action, flatten_authorized_user, user_has_permission, +}; + +/// Fetch all comments related to a case. id should be the 7 character short code. Requires Comments permission. +#[openapi(tag = "Admin")] +#[get("/comments/case/")] +pub async fn admin_comment_fetch_case( + db: &State, + auth: AdminAuthorization, + id: Reference, +) -> Result>> { + let user = flatten_authorized_user(&auth); + if !user_has_permission(user, v0::AdminUserPermissionFlags::Comments) { + return Err(create_error!(MissingPermission { + permission: "Comments".to_string() + })); + } + + let comments: Vec = db + .admin_comment_fetch_object_comments(&id.id) + .await? + .iter() + .map(|f| f.clone().into()) + .collect(); + + create_audit_action( + db, + &user.id, + v0::AdminAuditItemActions::CommentFetchForObject, + Some(&id.id), + None, + None, + ) + .await?; + + Ok(Json(comments)) +} diff --git a/crates/delta/src/routes/admin/comments/comment_fetch_object.rs b/crates/delta/src/routes/admin/comments/comment_fetch_object.rs new file mode 100644 index 00000000..3fdd3057 --- /dev/null +++ b/crates/delta/src/routes/admin/comments/comment_fetch_object.rs @@ -0,0 +1,43 @@ +use revolt_database::{util::reference::Reference, AdminAuthorization, Database}; +use revolt_models::v0::{self}; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; + +use crate::routes::admin::util::{ + create_audit_action, flatten_authorized_user, user_has_permission, +}; + +/// Fetch comments on an object. Requires Comments permission. +#[openapi(tag = "Admin")] +#[get("/comments/object/")] +pub async fn admin_comment_fetch_object( + db: &State, + auth: AdminAuthorization, + id: Reference, +) -> Result>> { + let user = flatten_authorized_user(&auth); + if !user_has_permission(user, v0::AdminUserPermissionFlags::Comments) { + return Err(create_error!(MissingPermission { + permission: "Comments".to_string() + })); + } + + let comments: Vec = db + .admin_comment_fetch_object_comments(&id.id) + .await? + .iter() + .map(|f| f.clone().into()) + .collect(); + + create_audit_action( + db, + &user.id, + v0::AdminAuditItemActions::CommentFetchForObject, + None, + Some(&id.id), + None, + ) + .await?; + + Ok(Json(comments)) +} diff --git a/crates/delta/src/routes/admin/comments/mod.rs b/crates/delta/src/routes/admin/comments/mod.rs new file mode 100644 index 00000000..b6c4adaf --- /dev/null +++ b/crates/delta/src/routes/admin/comments/mod.rs @@ -0,0 +1,4 @@ +pub mod comment_create; +pub mod comment_edit; +pub mod comment_fetch_case; +pub mod comment_fetch_object; diff --git a/crates/delta/src/routes/admin/meta/create_token.rs b/crates/delta/src/routes/admin/meta/create_token.rs index 11f16d20..d639cce9 100644 --- a/crates/delta/src/routes/admin/meta/create_token.rs +++ b/crates/delta/src/routes/admin/meta/create_token.rs @@ -18,7 +18,9 @@ pub async fn admin_create_token( &auth.on_behalf_of, v0::AdminUserPermissionFlags::CreateTokens, ) { - return Err(create_error!(NotFound)); + return Err(create_error!(MissingPermission { + permission: "CreateTokens".to_string() + })); } if body.expiry > Timestamp::now_utc() + Duration::days(30) { diff --git a/crates/delta/src/routes/admin/meta/create_user.rs b/crates/delta/src/routes/admin/meta/create_user.rs index 93e05f56..c8f2e917 100644 --- a/crates/delta/src/routes/admin/meta/create_user.rs +++ b/crates/delta/src/routes/admin/meta/create_user.rs @@ -15,7 +15,9 @@ pub async fn admin_create_user( ) -> Result> { let user = flatten_authorized_user(&auth); if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageAdminUsers) { - return Err(create_error!(NotFound)); + return Err(create_error!(MissingPermission { + permission: "ManageAdminUsers".to_string() + })); } // TODO: technically there's a privilege escalation here since anyone with the manageAdminUsers permission can assign whatever permissions they want. diff --git a/crates/delta/src/routes/admin/meta/edit_user.rs b/crates/delta/src/routes/admin/meta/edit_user.rs index 4d138cbf..68b982e5 100644 --- a/crates/delta/src/routes/admin/meta/edit_user.rs +++ b/crates/delta/src/routes/admin/meta/edit_user.rs @@ -17,7 +17,9 @@ pub async fn admin_edit_user( ) -> Result { let user = flatten_authorized_user(&auth); if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageAdminUsers) { - return Err(create_error!(NotFound)); + return Err(create_error!(MissingPermission { + permission: "ManageAdminUsers".to_string() + })); } // TODO: technically there's a privilege escalation here since anyone with the manageAdminUsers permission can assign whatever permissions they want. diff --git a/crates/delta/src/routes/admin/meta/revoke_token.rs b/crates/delta/src/routes/admin/meta/revoke_token.rs index 7df8ad19..4c0bea9c 100644 --- a/crates/delta/src/routes/admin/meta/revoke_token.rs +++ b/crates/delta/src/routes/admin/meta/revoke_token.rs @@ -19,7 +19,9 @@ pub async fn admin_revoke_token( &auth.on_behalf_of, v0::AdminUserPermissionFlags::CreateTokens, ) { - return Err(create_error!(NotFound)); + return Err(create_error!(MissingPermission { + permission: "CreateTokens".to_string() + })); } let token = token.as_admin_token(db).await?; diff --git a/crates/delta/src/routes/admin/mod.rs b/crates/delta/src/routes/admin/mod.rs index 127c1b6d..b2862ee8 100644 --- a/crates/delta/src/routes/admin/mod.rs +++ b/crates/delta/src/routes/admin/mod.rs @@ -3,24 +3,38 @@ use rocket::Route; mod accounts; mod cases; +mod comments; mod meta; mod reports; mod roles; mod search; +mod servers; mod users; -mod object_edit_note; -mod object_get_note; mod util; pub fn routes() -> (Vec, OpenApi) { openapi_get_routes_spec![ - object_edit_note::admin_object_edit_note, - object_get_note::admin_object_get_note, meta::create_user::admin_create_user, meta::edit_user::admin_edit_user, meta::fetch_users::admin_fetch_users, meta::create_token::admin_create_token, - meta::revoke_token::admin_revoke_token + meta::revoke_token::admin_revoke_token, + comments::comment_create::admin_comment_create, + comments::comment_edit::admin_comment_edit, + comments::comment_fetch_case::admin_comment_fetch_case, + comments::comment_fetch_object::admin_comment_fetch_object, + servers::fetch::server_get::admin_server_get, + servers::fetch::server_get_participants::admin_server_get_participants, + servers::fetch::server_get_all_members::admin_server_get_members, + servers::actions::server_add_members::admin_server_add_member, + servers::actions::server_ban_members::admin_server_ban_member, + servers::actions::server_unban_members::admin_server_unban_member, + servers::actions::server_change_owner::admin_server_change_owner, + servers::actions::server_create_invite::admin_server_create_invite, + servers::actions::server_delete_invites::admin_server_delete_invites, + servers::actions::server_delete::admin_server_delete, + servers::actions::server_edit::admin_server_edit, + servers::actions::server_remove_members::admin_server_remove_members ] } diff --git a/crates/delta/src/routes/admin/object_edit_note.rs b/crates/delta/src/routes/admin/object_edit_note.rs deleted file mode 100644 index fb619f6f..00000000 --- a/crates/delta/src/routes/admin/object_edit_note.rs +++ /dev/null @@ -1,28 +0,0 @@ -use revolt_database::{util::reference::Reference, AdminAuthorization, AdminObjectNote, Database}; -use revolt_models::v0::{self}; -use revolt_result::{create_error, Result}; -use rocket::{serde::json::Json, State}; -use rocket_empty::EmptyResponse; - -use crate::routes::admin::util::{flatten_authorized_user, user_has_permission}; - -/// Edit an object note. Requires ObjectNotes permission. -#[openapi(tag = "Admin")] -#[post("/notes/", data = "")] -pub async fn admin_object_edit_note( - db: &State, - auth: AdminAuthorization, - object: Reference, - body: Json, -) -> Result { - let user = flatten_authorized_user(&auth); - if !user_has_permission(user, v0::AdminUserPermissionFlags::ObjectNotes) { - return Err(create_error!(NotFound)); - } - - // Unfortunately due to how our system works, we can't limit users to editing objects they have permissions for (eg users, servers, etc.). - // Hence it being tied to its own permission - db.admin_note_update(AdminObjectNote::new(&object.id, &user.id, &body.content)) - .await?; - Ok(EmptyResponse) -} diff --git a/crates/delta/src/routes/admin/object_get_note.rs b/crates/delta/src/routes/admin/object_get_note.rs deleted file mode 100644 index 2b3cf0b3..00000000 --- a/crates/delta/src/routes/admin/object_get_note.rs +++ /dev/null @@ -1,24 +0,0 @@ -use revolt_database::{util::reference::Reference, AdminAuthorization, Database}; -use revolt_models::v0::{self}; -use revolt_result::{create_error, Result}; -use rocket::{serde::json::Json, State}; - -use crate::routes::admin::util::{flatten_authorized_user, user_has_permission}; - -/// Edit an object note. Requires ObjectNotes permission. -#[openapi(tag = "Admin")] -#[get("/notes/")] -pub async fn admin_object_get_note( - db: &State, - auth: AdminAuthorization, - object: Reference, -) -> Result> { - let user = flatten_authorized_user(&auth); - if !user_has_permission(user, v0::AdminUserPermissionFlags::ObjectNotes) { - return Err(create_error!(NotFound)); - } - - // Unfortunately due to how our system works, we can't limit users to editing objects they have permissions for (eg users, servers, etc.). - // Hence it being tied to its own permission - Ok(Json(db.admin_note_fetch(&object.id).await?.into())) -} diff --git a/crates/delta/src/routes/admin/servers/actions/mod.rs b/crates/delta/src/routes/admin/servers/actions/mod.rs index e69de29b..aadd11d8 100644 --- a/crates/delta/src/routes/admin/servers/actions/mod.rs +++ b/crates/delta/src/routes/admin/servers/actions/mod.rs @@ -0,0 +1,9 @@ +pub mod server_add_members; +pub mod server_ban_members; +pub mod server_change_owner; +pub mod server_create_invite; +pub mod server_delete; +pub mod server_delete_invites; +pub mod server_edit; +pub mod server_remove_members; +pub mod server_unban_members; diff --git a/crates/delta/src/routes/admin/servers/actions/server_add_members.rs b/crates/delta/src/routes/admin/servers/actions/server_add_members.rs index e69de29b..86bfde23 100644 --- a/crates/delta/src/routes/admin/servers/actions/server_add_members.rs +++ b/crates/delta/src/routes/admin/servers/actions/server_add_members.rs @@ -0,0 +1,48 @@ +use revolt_database::{util::reference::Reference, AdminAuthorization, Database, Member}; +use revolt_models::v0::{self}; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; + +use crate::routes::admin::util::{ + create_audit_action, flatten_authorized_user, user_has_permission, +}; + +/// # Add server member +/// +/// Add a user to a server, optionally disabling notifications +#[openapi(tag = "Admin")] +#[post("/servers//members?&&")] +pub async fn admin_server_add_member( + db: &State, + auth: AdminAuthorization, + id: Reference, + case: Option<&str>, + user_id: Reference, + suppress_alerts: bool, +) -> Result> { + let user = flatten_authorized_user(&auth); + if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageServers) { + return Err(create_error!(MissingPermission { + permission: "ManageServers".to_string() + })); + } + + let server = id.as_server(db).await?; + let user = user_id.as_user(db).await?; + let (member, _) = Member::create(db, &server, &user, None, !suppress_alerts).await?; + + create_audit_action( + db, + &user.id, + v0::AdminAuditItemActions::ServerAddMember, + case, + Some(&id.id), + None, + ) + .await?; + + Ok(Json(v0::MemberWithUserResponse { + user: user.into_self(false).await, + member: member.into(), + })) +} diff --git a/crates/delta/src/routes/admin/servers/actions/server_ban_members.rs b/crates/delta/src/routes/admin/servers/actions/server_ban_members.rs new file mode 100644 index 00000000..8ed09526 --- /dev/null +++ b/crates/delta/src/routes/admin/servers/actions/server_ban_members.rs @@ -0,0 +1,83 @@ +use revolt_database::{util::reference::Reference, AdminAuthorization, Database, ServerBan}; +use revolt_models::v0::{self}; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; +use validator::Validate; + +use crate::routes::admin::util::{ + create_audit_action, flatten_authorized_user, user_has_permission, +}; + +/// # Ban a server member +/// +/// Ban a member who is not the owner +#[openapi(tag = "Admin")] +#[post( + "/servers//ban?&&", + data = "" +)] +pub async fn admin_server_ban_member( + db: &State, + auth: AdminAuthorization, + id: Reference, + case: Option<&str>, + user_id: Reference, + suppress_alerts: bool, + body: Json, +) -> Result> { + let user = flatten_authorized_user(&auth); + if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageServers) { + return Err(create_error!(MissingPermission { + permission: "ManageServers".to_string() + })); + } + + let body = body.into_inner(); + body.validate().map_err(|error| { + create_error!(FailedValidation { + error: error.to_string() + }) + })?; + + let server = id.as_server(db).await?; + let target = user_id.as_user(db).await?; + + if target.id == server.owner { + return Err(create_error!(InvalidOperation)); + } + + let resp = if let Ok(member) = user_id.as_member(db, &id.id).await { + member + .remove( + db, + &server, + revolt_database::RemovalIntention::Ban, + suppress_alerts, + ) + .await?; + + Ok(ServerBan::create(db, &server, &target.id, body.reason.clone()).await?) + } else { + Err(create_error!(NotAMember)) + }?; + + let context = format!( + "user id: {:}, server id: {:}, reason: {:}", + &user_id.id, + &id.id, + body.reason + .clone() + .unwrap_or_else(|| "None Provided".to_string()) + ); + create_audit_action( + db, + &user.id, + v0::AdminAuditItemActions::ServerBanMember, + case, + Some(&id.id), + Some(&context), + ) + .await?; + + Ok(Json(resp.into())) +} diff --git a/crates/delta/src/routes/admin/servers/actions/server_change_owner.rs b/crates/delta/src/routes/admin/servers/actions/server_change_owner.rs index e69de29b..dcb9da4c 100644 --- a/crates/delta/src/routes/admin/servers/actions/server_change_owner.rs +++ b/crates/delta/src/routes/admin/servers/actions/server_change_owner.rs @@ -0,0 +1,55 @@ +use revolt_database::{util::reference::Reference, AdminAuthorization, Database, PartialServer}; +use revolt_models::v0::{self}; +use revolt_result::{create_error, Result}; +use rocket::State; +use rocket_empty::EmptyResponse; + +use crate::routes::admin::util::{ + create_audit_action, flatten_authorized_user, user_has_permission, +}; + +/// # Change Server Owner +/// +/// Change the ownership of a server. +#[openapi(tag = "Admin")] +#[post("/servers//owner?&")] +pub async fn admin_server_change_owner( + db: &State, + auth: AdminAuthorization, + id: Reference, + case: Option<&str>, + user_id: Reference, +) -> Result { + let user = flatten_authorized_user(&auth); + if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageServers) { + return Err(create_error!(MissingPermission { + permission: "ManageServers".to_string() + })); + } + + let server = id.as_server(db).await?; + let target = user_id.as_user(db).await?; + + if target.id == server.owner { + return Err(create_error!(InvalidOperation)); + } + + let partial = PartialServer { + owner: Some(target.id), + ..Default::default() + }; + db.update_server(&server.id, &partial, vec![]).await?; + + let context = format!("user id: {:}, server id: {:}", &user_id.id, &id.id); + create_audit_action( + db, + &user.id, + v0::AdminAuditItemActions::ServerChangeOwner, + case, + Some(&id.id), + Some(&context), + ) + .await?; + + Ok(EmptyResponse) +} diff --git a/crates/delta/src/routes/admin/servers/actions/server_create_invite.rs b/crates/delta/src/routes/admin/servers/actions/server_create_invite.rs index e69de29b..f6d734a1 100644 --- a/crates/delta/src/routes/admin/servers/actions/server_create_invite.rs +++ b/crates/delta/src/routes/admin/servers/actions/server_create_invite.rs @@ -0,0 +1,66 @@ +use revolt_database::{util::reference::Reference, AdminAuthorization, Database, Invite}; +use revolt_models::v0::{self}; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; + +use crate::routes::admin::util::{ + create_audit_action, flatten_authorized_user, user_has_permission, +}; + +/// # Create Server Invite +/// +/// Create an invite to a server. Optionally include a slug to create a vanity URL. +#[openapi(tag = "Admin")] +#[post("/servers//invites?&&")] +pub async fn admin_server_create_invite( + db: &State, + auth: AdminAuthorization, + id: Reference, + channel_id: Reference, + case: Option<&str>, + slug: Option<&str>, +) -> Result> { + let user = flatten_authorized_user(&auth); + if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageServers) { + return Err(create_error!(MissingPermission { + permission: "ManageServers".to_string() + })); + } + + let server = id.as_server(db).await?; + let channel = channel_id.as_channel(db).await?; + let creator = db.fetch_user(&server.owner).await?; + + let invite: Invite; + + if let Some(slug) = slug { + invite = Invite::Server { + code: slug.to_string(), + creator: creator.id.clone(), + server: server.id.clone(), + channel: channel.id().to_string(), + }; + #[allow(clippy::disallowed_methods)] + db.insert_invite(&invite).await?; + } else { + invite = Invite::create_channel_invite(db, &creator, &channel).await?; + } + + let context = format!( + "server id: {:}, invite slug: {:}, channel: {:}", + &id.id, + invite.code(), + channel.id() + ); + create_audit_action( + db, + &user.id, + v0::AdminAuditItemActions::ServerCreateInvite, + case, + Some(&id.id), + Some(&context), + ) + .await?; + + Ok(Json(invite.into())) +} diff --git a/crates/delta/src/routes/admin/servers/actions/server_delete.rs b/crates/delta/src/routes/admin/servers/actions/server_delete.rs index e69de29b..022c2b36 100644 --- a/crates/delta/src/routes/admin/servers/actions/server_delete.rs +++ b/crates/delta/src/routes/admin/servers/actions/server_delete.rs @@ -0,0 +1,41 @@ +use revolt_database::{util::reference::Reference, AdminAuthorization, Database}; +use revolt_models::v0::{self}; +use revolt_result::{create_error, Result}; +use rocket::State; +use rocket_empty::EmptyResponse; + +use crate::routes::admin::util::{ + create_audit_action, flatten_authorized_user, user_has_permission, +}; + +/// Get a list of admin users. Any active user may use this endpoint. +/// Typically the client should cache this data. +#[openapi(tag = "Admin")] +#[delete("/servers/?")] +pub async fn admin_server_delete( + db: &State, + auth: AdminAuthorization, + id: Reference, + case: Option<&str>, +) -> Result { + let user = flatten_authorized_user(&auth); + if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageServers) { + return Err(create_error!(MissingPermission { + permission: "ManageServers".to_string() + })); + } + + db.delete_server(&id.id).await?; + + create_audit_action( + db, + &user.id, + v0::AdminAuditItemActions::ServerDelete, + case, + Some(&id.id), + None, + ) + .await?; + + Ok(EmptyResponse) +} diff --git a/crates/delta/src/routes/admin/servers/actions/server_delete_invites.rs b/crates/delta/src/routes/admin/servers/actions/server_delete_invites.rs index e69de29b..a5838c01 100644 --- a/crates/delta/src/routes/admin/servers/actions/server_delete_invites.rs +++ b/crates/delta/src/routes/admin/servers/actions/server_delete_invites.rs @@ -0,0 +1,62 @@ +use revolt_database::{util::reference::Reference, AdminAuthorization, Database}; +use revolt_models::v0::{self}; +use revolt_result::{create_error, Result}; +use rocket::State; +use rocket_empty::EmptyResponse; + +use crate::routes::admin::util::{ + create_audit_action, flatten_authorized_user, user_has_permission, +}; + +/// Delete Server Invite +/// +/// Delete a server invite, or all of a server's invites. +#[openapi(tag = "Admin")] +#[delete("/servers//invites?&")] +pub async fn admin_server_delete_invites( + db: &State, + auth: AdminAuthorization, + id: Reference, + case: Option<&str>, + slug: Option, +) -> Result { + let user = flatten_authorized_user(&auth); + if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageServers) { + return Err(create_error!(MissingPermission { + permission: "ManageServers".to_string() + })); + } + + if let Some(slug) = slug { + let invite = slug.as_invite(db).await?; + + db.delete_invite(&slug.id).await?; + + let context = format!("server id: {:}, invite slug: {:}", &id.id, invite.code()); + create_audit_action( + db, + &user.id, + v0::AdminAuditItemActions::ServerDeleteInvite, + case, + Some(&id.id), + Some(&context), + ) + .await?; + } else { + let invites = db.fetch_invites_for_server(&id.id).await?; + for invite in invites { + db.delete_invite(invite.code()).await?; + } + create_audit_action( + db, + &user.id, + v0::AdminAuditItemActions::ServerDeleteAllInvites, + case, + Some(&id.id), + None, + ) + .await?; + } + + Ok(EmptyResponse) +} diff --git a/crates/delta/src/routes/admin/servers/actions/server_edit.rs b/crates/delta/src/routes/admin/servers/actions/server_edit.rs index e69de29b..0e1f465f 100644 --- a/crates/delta/src/routes/admin/servers/actions/server_edit.rs +++ b/crates/delta/src/routes/admin/servers/actions/server_edit.rs @@ -0,0 +1,52 @@ +use revolt_database::{util::reference::Reference, AdminAuthorization, Database}; +use revolt_models::v0::{self}; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; +use validator::Validate; + +use crate::routes::admin::util::{ + create_audit_action, flatten_authorized_user, user_has_permission, +}; + +/// # Edit server +/// +/// Edit any attributes of a server. +#[openapi(tag = "Admin")] +#[patch("/servers/?", data = "")] +pub async fn admin_server_edit( + db: &State, + auth: AdminAuthorization, + id: Reference, + case: Option<&str>, + body: Json, +) -> Result> { + let user = flatten_authorized_user(&auth); + if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageServers) { + return Err(create_error!(MissingPermission { + permission: "ManageServers".to_string() + })); + } + + let body = body.into_inner(); + body.validate().map_err(|error| { + create_error!(FailedValidation { + error: error.to_string() + }) + })?; + + let mut server = id.as_server(db).await?; + let user = db.fetch_user(&server.owner).await?; + crate::routes::servers::server_edit::edit_data(body, db, &mut server, &user).await?; + + create_audit_action( + db, + &user.id, + v0::AdminAuditItemActions::ServerEdit, + case, + Some(&id.id), + None, + ) + .await?; + + Ok(Json(server.into())) +} diff --git a/crates/delta/src/routes/admin/servers/actions/server_remove_members.rs b/crates/delta/src/routes/admin/servers/actions/server_remove_members.rs index e69de29b..c17779aa 100644 --- a/crates/delta/src/routes/admin/servers/actions/server_remove_members.rs +++ b/crates/delta/src/routes/admin/servers/actions/server_remove_members.rs @@ -0,0 +1,61 @@ +use revolt_database::{util::reference::Reference, AdminAuthorization, Database}; +use revolt_models::v0::{self}; +use revolt_result::{create_error, Result}; +use rocket::State; +use rocket_empty::EmptyResponse; + +use crate::routes::admin::util::{ + create_audit_action, flatten_authorized_user, user_has_permission, +}; + +/// # Ban a server member +/// +/// Ban a member who is not the owner +#[openapi(tag = "Admin")] +#[post("/servers//remove?&&")] +pub async fn admin_server_remove_members( + db: &State, + auth: AdminAuthorization, + id: Reference, + case: Option<&str>, + user_id: Vec, + suppress_alerts: bool, +) -> Result { + let admin_user = flatten_authorized_user(&auth); + if !user_has_permission(admin_user, v0::AdminUserPermissionFlags::ManageServers) { + return Err(create_error!(MissingPermission { + permission: "ManageServers".to_string() + })); + } + + let server = id.as_server(db).await?; + for uid in user_id { + let target = uid.as_member(db, &id.id).await?; + + if target.id.user == server.owner { + return Err(create_error!(InvalidOperation)); + } + + target + .remove( + db, + &server, + revolt_database::RemovalIntention::Kick, + suppress_alerts, + ) + .await?; + + let context = format!("user id: {:}, server id: {:}", &uid.id, &id.id); + create_audit_action( + db, + &admin_user.id, + v0::AdminAuditItemActions::ServerRemoveMember, + case, + Some(&id.id), + Some(&context), + ) + .await?; + } + + Ok(EmptyResponse) +} diff --git a/crates/delta/src/routes/admin/servers/actions/server_set_flags.rs b/crates/delta/src/routes/admin/servers/actions/server_set_flags.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/crates/delta/src/routes/admin/servers/actions/server_unban_members.rs b/crates/delta/src/routes/admin/servers/actions/server_unban_members.rs new file mode 100644 index 00000000..622a0194 --- /dev/null +++ b/crates/delta/src/routes/admin/servers/actions/server_unban_members.rs @@ -0,0 +1,60 @@ +use revolt_database::{util::reference::Reference, AdminAuthorization, Database}; +use revolt_models::v0::{self}; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; +use rocket_empty::EmptyResponse; + +use crate::routes::admin::util::{ + create_audit_action, flatten_authorized_user, user_has_permission, +}; + +/// Get a list of admin users. Any active user may use this endpoint. +/// Typically the client should cache this data. +#[openapi(tag = "Admin")] +#[post( + "/servers//unban?&&", + data = "" +)] +pub async fn admin_server_unban_member( + db: &State, + auth: AdminAuthorization, + id: Reference, + case: Option<&str>, + user_id: Reference, + suppress_alerts: bool, + body: Json, +) -> Result { + let user = flatten_authorized_user(&auth); + if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageServers) { + return Err(create_error!(MissingPermission { + permission: "ManageServers".to_string() + })); + } + + let ban = db + .fetch_ban(&id.id, &user_id.id) + .await + .map_err(|_| create_error!(NotFound))?; + + db.delete_ban(&ban.id).await?; + + let context = format!( + "user id: {:}, server id: {:}, reason: {:}", + &user_id.id, + &id.id, + body.reason + .clone() + .unwrap_or_else(|| "None Provided".to_string()) + ); + create_audit_action( + db, + &user.id, + v0::AdminAuditItemActions::ServerUnbanMember, + case, + Some(&id.id), + Some(&context), + ) + .await?; + + Ok(EmptyResponse) +} diff --git a/crates/delta/src/routes/admin/servers/fetch/mod.rs b/crates/delta/src/routes/admin/servers/fetch/mod.rs index e69de29b..279ae510 100644 --- a/crates/delta/src/routes/admin/servers/fetch/mod.rs +++ b/crates/delta/src/routes/admin/servers/fetch/mod.rs @@ -0,0 +1,3 @@ +pub mod server_get; +pub mod server_get_all_members; +pub mod server_get_participants; diff --git a/crates/delta/src/routes/admin/servers/fetch/server_get.rs b/crates/delta/src/routes/admin/servers/fetch/server_get.rs index e69de29b..5794a487 100644 --- a/crates/delta/src/routes/admin/servers/fetch/server_get.rs +++ b/crates/delta/src/routes/admin/servers/fetch/server_get.rs @@ -0,0 +1,51 @@ +use revolt_database::{util::reference::Reference, AdminAuthorization, Database}; +use revolt_models::v0::{self}; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; + +use crate::routes::admin::util::{ + create_audit_action, flatten_authorized_user, user_has_permission, +}; + +/// Get a list of admin users. Any active user may use this endpoint. +/// Typically the client should cache this data. +#[openapi(tag = "Admin")] +#[get("/servers/?")] +pub async fn admin_server_get( + db: &State, + auth: AdminAuthorization, + id: Reference, + case: Option<&str>, +) -> Result> { + let user = flatten_authorized_user(&auth); + if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageServers) { + return Err(create_error!(MissingPermission { + permission: "ManageServers".to_string() + })); + } + + let server = id.as_server(db).await?; + let owner = db.fetch_user(&server.owner).await?.into_self(false).await; + let comments: Vec = db + .admin_comment_fetch_object_comments(&server.id) + .await? + .iter() + .map(|f| f.clone().into()) + .collect(); + + create_audit_action( + db, + &user.id, + v0::AdminAuditItemActions::ServerFetch, + case, + Some(&id.id), + None, + ) + .await?; + + Ok(Json(v0::AdminServerResponse { + server: server.into(), + owner, + comments, + })) +} diff --git a/crates/delta/src/routes/admin/servers/fetch/server_get_all_members.rs b/crates/delta/src/routes/admin/servers/fetch/server_get_all_members.rs new file mode 100644 index 00000000..75881f69 --- /dev/null +++ b/crates/delta/src/routes/admin/servers/fetch/server_get_all_members.rs @@ -0,0 +1,67 @@ +use futures::future::join_all; +use iso8601_timestamp::Timestamp; +use revolt_database::{util::reference::Reference, AdminAuthorization, Database}; +use revolt_models::v0::{self}; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; + +use crate::routes::admin::util::{ + create_audit_action, flatten_authorized_user, user_has_permission, +}; + +/// Get a list of admin users. Any active user may use this endpoint. +/// Typically the client should cache this data. +#[openapi(tag = "Admin")] +#[get("/servers//members?&")] +pub async fn admin_server_get_members( + db: &State, + auth: AdminAuthorization, + id: Reference, + case: Option<&str>, + after: Option, +) -> Result> { + let user = flatten_authorized_user(&auth); + if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageServers) { + return Err(create_error!(MissingPermission { + permission: "ManageServers".to_string() + })); + } + + let server = id.as_server(db).await?; + let members = db.fetch_server_members(&server.id, 100, after).await?; + + create_audit_action( + db, + &user.id, + v0::AdminAuditItemActions::ServerFetchMembers, + case, + Some(&id.id), + None, + ) + .await?; + + let members_and_users = join_all(members.iter().map(|(u, m)| async move { + let user: v0::User = u.clone().into_self(false).await; + let member: v0::Member = m.clone().into(); + v0::MemberWithUserResponse { user, member } + })) + .await; + + let mut resp = v0::AdminMemberWithUserAndOffsetResponse { + after: None, + users: vec![], + }; + + if let Some(last) = members_and_users.last() { + resp.after = Some( + last.member + .joined_at + .duration_since(Timestamp::UNIX_EPOCH) + .whole_milliseconds() as usize, + ); + } + + resp.users = members_and_users; + + Ok(Json(resp)) +} diff --git a/crates/delta/src/routes/admin/servers/fetch/server_get_participants.rs b/crates/delta/src/routes/admin/servers/fetch/server_get_participants.rs index e69de29b..bcd43a37 100644 --- a/crates/delta/src/routes/admin/servers/fetch/server_get_participants.rs +++ b/crates/delta/src/routes/admin/servers/fetch/server_get_participants.rs @@ -0,0 +1,51 @@ +use futures::future::join_all; +use revolt_database::{util::reference::Reference, AdminAuthorization, Database}; +use revolt_models::v0::{self}; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; + +use crate::routes::admin::util::{ + create_audit_action, flatten_authorized_user, user_has_permission, +}; + +/// Get a list of admin users. Any active user may use this endpoint. +/// Typically the client should cache this data. +#[openapi(tag = "Admin")] +#[get("/servers//participants?")] +pub async fn admin_server_get_participants( + db: &State, + auth: AdminAuthorization, + id: Reference, + case: Option<&str>, +) -> Result)>>> { + let user = flatten_authorized_user(&auth); + if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageServers) { + return Err(create_error!(MissingPermission { + permission: "ManageServers".to_string() + })); + } + + let server = id.as_server(db).await?; + let participants = db.fetch_server_participants(&server.id).await?; + + create_audit_action( + db, + &user.id, + v0::AdminAuditItemActions::ServerFetchParticipants, + case, + Some(&id.id), + None, + ) + .await?; + + println!("{:?}", &participants); + + let resp = join_all(participants.iter().map(|(u, m)| async move { + let user: v0::User = u.clone().into_self(false).await; + let member: Option = m.clone().map(|mu| mu.into()); + (user, member) + })) + .await; + + Ok(Json(resp)) +} diff --git a/crates/delta/src/routes/admin/servers/mod.rs b/crates/delta/src/routes/admin/servers/mod.rs index f6d4d6e6..d0a73f79 100644 --- a/crates/delta/src/routes/admin/servers/mod.rs +++ b/crates/delta/src/routes/admin/servers/mod.rs @@ -1,2 +1,2 @@ -mod actions; -mod fetch; +pub mod actions; +pub mod fetch; diff --git a/crates/delta/src/routes/admin/util.rs b/crates/delta/src/routes/admin/util.rs index 0fc72128..479a7f9e 100644 --- a/crates/delta/src/routes/admin/util.rs +++ b/crates/delta/src/routes/admin/util.rs @@ -1,5 +1,9 @@ -use revolt_database::{AdminAuthorization, AdminUser, AdminUserPermissionFlagsValue}; -use revolt_models::v0::AdminUserPermissionFlags; +use revolt_database::{ + AdminAuditItem, AdminAuthorization, AdminComment, AdminUser, AdminUserPermissionFlagsValue, + Database, +}; +use revolt_models::v0::{AdminAuditItemActions, AdminUserPermissionFlags}; +use revolt_result::Result; pub fn user_has_permission(user: &AdminUser, permission: AdminUserPermissionFlags) -> bool { let flags = AdminUserPermissionFlagsValue(user.permissions); @@ -7,9 +11,40 @@ pub fn user_has_permission(user: &AdminUser, permission: AdminUserPermissionFlag flags.has(permission) } -pub fn flatten_authorized_user<'a>(auth: &'a AdminAuthorization) -> &'a AdminUser { +pub fn flatten_authorized_user(auth: &AdminAuthorization) -> &AdminUser { match auth { - AdminAuthorization::AdminUser(admin_user) => &admin_user, + AdminAuthorization::AdminUser(admin_user) => admin_user, AdminAuthorization::AdminMachine(admin_machine_token) => &admin_machine_token.on_behalf_of, } } + +/// Creates audit logs, and if applicable, adds a comment to the referenced case. +pub async fn create_audit_action( + db: &Database, + mod_id: &str, + action: AdminAuditItemActions, + short_case_id: Option<&str>, + target_id: Option<&str>, + context: Option<&str>, +) -> Result<()> { + let audit = AdminAuditItem::new(mod_id, action.clone(), short_case_id, target_id, context); + db.admin_audit_insert(audit).await?; + + if action.makes_comment() { + if let Some(short_case_id) = short_case_id { + if let Some(target_id) = target_id { + let case = db.admin_case_fetch_from_shorthand(short_case_id).await?; + db.admin_comment_insert(AdminComment::new_system_message( + &case.id, + mod_id, + short_case_id, + action, + context, + target_id, + )) + .await?; + } + } + } + Ok(()) +} diff --git a/crates/delta/src/routes/bots/invite.rs b/crates/delta/src/routes/bots/invite.rs index 10c09eed..f1ea7c3d 100644 --- a/crates/delta/src/routes/bots/invite.rs +++ b/crates/delta/src/routes/bots/invite.rs @@ -43,7 +43,7 @@ pub async fn invite_bot( .await .throw_if_lacking_channel_permission(ChannelPermission::ManageServer)?; - Member::create(db, &server, &bot_user, None) + Member::create(db, &server, &bot_user, None, true) .await .map(|_| EmptyResponse) } diff --git a/crates/delta/src/routes/invites/invite_join.rs b/crates/delta/src/routes/invites/invite_join.rs index 83b59195..0999eb1f 100644 --- a/crates/delta/src/routes/invites/invite_join.rs +++ b/crates/delta/src/routes/invites/invite_join.rs @@ -24,7 +24,7 @@ pub async fn join( match &invite { Invite::Server { server, .. } => { let server = db.fetch_server(server).await?; - let (_, channels) = Member::create(db, &server, &user, None).await?; + let (_, channels) = Member::create(db, &server, &user, None, true).await?; Ok(Json(InviteJoinResponse::Server { channels: channels.into_iter().map(|c| c.into()).collect(), diff --git a/crates/delta/src/routes/servers/mod.rs b/crates/delta/src/routes/servers/mod.rs index a7553625..12571840 100644 --- a/crates/delta/src/routes/servers/mod.rs +++ b/crates/delta/src/routes/servers/mod.rs @@ -21,7 +21,7 @@ mod roles_fetch; mod server_ack; mod server_create; mod server_delete; -mod server_edit; +pub mod server_edit; mod server_fetch; pub fn routes() -> (Vec, OpenApi) { diff --git a/crates/delta/src/routes/servers/server_create.rs b/crates/delta/src/routes/servers/server_create.rs index 82f0a8aa..a2ba1c45 100644 --- a/crates/delta/src/routes/servers/server_create.rs +++ b/crates/delta/src/routes/servers/server_create.rs @@ -30,7 +30,7 @@ pub async fn create_server( user.can_acquire_server(db).await?; let (server, channels) = Server::create(db, data, &user, true).await?; - let (_, channels) = Member::create(db, &server, &user, Some(channels)).await?; + let (_, channels) = Member::create(db, &server, &user, Some(channels), true).await?; Ok(Json(v0::CreateServerLegacyResponse { server: server.into(), diff --git a/crates/delta/src/routes/servers/server_edit.rs b/crates/delta/src/routes/servers/server_edit.rs index 05688473..eb67624c 100644 --- a/crates/delta/src/routes/servers/server_edit.rs +++ b/crates/delta/src/routes/servers/server_edit.rs @@ -2,7 +2,7 @@ use std::collections::HashSet; use revolt_database::{ util::{permissions::DatabasePermissionQuery, reference::Reference}, - Database, File, PartialServer, User, + Database, File, PartialServer, Server, User, }; use revolt_models::v0; use revolt_permissions::{calculate_server_permissions, ChannelPermission}; @@ -69,6 +69,16 @@ pub async fn edit( permissions.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?; } + edit_data(data, db, &mut server, &user).await?; + Ok(Json(server.into())) +} + +pub async fn edit_data( + data: v0::DataEditServer, + db: &Database, + server: &mut Server, + user: &User, +) -> Result<()> { let v0::DataEditServer { name, description, @@ -158,5 +168,5 @@ pub async fn edit( ) .await?; - Ok(Json(server.into())) + Ok(()) } diff --git a/crates/delta/src/util/test.rs b/crates/delta/src/util/test.rs index 98254e47..9b75d862 100644 --- a/crates/delta/src/util/test.rs +++ b/crates/delta/src/util/test.rs @@ -167,7 +167,7 @@ impl TestHarness { server: &Server, channels: Vec, ) -> (Channel, Member, Message) { - let (member, channels) = Member::create(&self.db, server, user, Some(channels)) + let (member, channels) = Member::create(&self.db, server, user, Some(channels), true) .await .expect("Failed to create member"); let channel = &channels[0]; diff --git a/scripts/email_users_via_zammad.py b/scripts/email_users_via_zammad.py new file mode 100644 index 00000000..e9eb2a66 --- /dev/null +++ b/scripts/email_users_via_zammad.py @@ -0,0 +1,52 @@ +# requires httpx off pypi +import httpx +import math +from os import getenv +import asyncio + +URL = getenv("ZAMMAD_URL") +TOKEN = getenv("ZAMMAD_TOKEN") +EMAIL_FILE = getenv("EMAILS_FILE") +PAYLOAD_TEMPLATE = getenv("PAYLOAD_FILE") + +assert URL and TOKEN and EMAIL_FILE and PAYLOAD_TEMPLATE, ValueError("Expected env variables EMAIL_FILE, PAYLOAD_FILE, ZAMMAD_URL and ZAMMAD_TOKEN.") + +URL = URL.strip("/") + "/api/v1/tickets" + +async def worker(payload: dict, emails: list[str]) -> None: + async with httpx.AsyncClient(headers={"Authorization": f"Token token={TOKEN}"}) as client: + assert URL # redundant but makes pyright happy + + for email in emails: + payload["customer_id"] = f"guess:{email}" + payload["article"]["to"] = email + print(email) + resp = await client.post(URL, json=payload) + if resp.status_code > 300: + raise RuntimeError(f"Failed to create ticket: {resp.status_code}\n{resp.read()}") + +async def main(): + assert EMAIL_FILE and PAYLOAD_TEMPLATE # pyright happy time + + with open(EMAIL_FILE) as f: + emails = [x.strip() for x in f.read().split("\n")] + + with open(PAYLOAD_TEMPLATE) as f: + p = {} + exec(f.read(), p) + payload = p["PAYLOAD"] + + # WORKER_COUNT = min(max(round(len(emails) / 10), 1), 10) + # EMAILS_PER = math.ceil(len(emails) / WORKER_COUNT) + # print(WORKER_COUNT, EMAILS_PER) + # tasks = [] + + # for worker_id in range(WORKER_COUNT): + # worker_emails = emails[EMAILS_PER * worker_id:EMAILS_PER * worker_id + 1] + # tasks.append(asyncio.create_task(worker(payload, worker_emails))) + + # await asyncio.gather(*tasks) + + await worker(payload, emails) + +asyncio.run(main()) \ No newline at end of file