From 03db5b1ddf0f7eda6ac2dabf2a7905fd82cd1baa Mon Sep 17 00:00:00 2001 From: IAmTomahawkx Date: Sun, 8 Jun 2025 01:18:07 -0700 Subject: [PATCH] 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.