Addd admin models to database

This commit is contained in:
IAmTomahawkx
2025-06-08 01:18:07 -07:00
committed by Angelo
parent af78ac0586
commit 6ecfd14b07
48 changed files with 1174 additions and 4 deletions

View File

@@ -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<Mutex<BTreeMap<String, AdminAuditItem>>>,
pub admin_cases: Arc<Mutex<HashMap<String, AdminCase>>>,
pub admin_case_comments: Arc<Mutex<HashMap<String, AdminCaseComment>>>,
pub admin_object_notes: Arc<Mutex<HashMap<String, AdminObjectNote>>>,
pub admin_strikes: Arc<Mutex<HashMap<String, AdminStrike>>>,
pub admin_tokens: Arc<Mutex<HashMap<String, AdminToken>>>,
pub admin_users: Arc<Mutex<HashMap<String, AdminUser>>>,
pub bots: Arc<Mutex<HashMap<String, Bot>>>,
pub channels: Arc<Mutex<HashMap<String, Channel>>>,
pub channel_invites: Arc<Mutex<HashMap<String, Invite>>>,

View File

@@ -0,0 +1,5 @@
mod models;
mod ops;
pub use models::*;
pub use ops::*;

View File

@@ -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<String>,
/// The object the action was taken against, if applicable
pub target: Option<String>,
/// The context of the action, if applicable (eg. search phrases)
pub context: Option<String>,
}
}

View File

@@ -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<Vec<AdminAuditItem>>;
}

View File

@@ -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<Vec<AdminAuditItem>> {
if let Some(before) = before_id {
Ok(self
.col::<AdminAuditItem>(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::<AdminAuditItem>(COL)
.find(doc! {})
.sort(doc! {"_id": -1})
.limit(limit)
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|f| f.ok())
.collect()
.await)
}
}
}

View File

@@ -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<Vec<AdminAuditItem>> {
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())
}
}
}

View File

@@ -0,0 +1,5 @@
mod models;
mod ops;
pub use models::*;
pub use ops::*;

View File

@@ -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<String>,
/// The content
pub content: String
},
"PartialAdminCaseComment"
}

View File

@@ -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<Vec<AdminCaseComment>>;
}

View File

@@ -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<Vec<AdminCaseComment>> {
query!(self, find, COL, doc! {"case_id": case_id})
}
}

View File

@@ -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<Vec<AdminCaseComment>> {
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())
}
}

View File

@@ -0,0 +1,5 @@
mod models;
mod ops;
pub use models::*;
pub use ops::*;

View File

@@ -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<String>,
/// The tags for the case
pub tags: Vec<String>,
},
"PartialAdminCase"
}

View File

@@ -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<AdminCase>;
/// 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<Vec<String>>,
before_id: Option<&str>,
limit: i64,
) -> Result<Vec<AdminCase>>;
}

View File

@@ -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::<Document>("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<AdminCase> {
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<Vec<String>>,
before_id: Option<&str>,
limit: i64,
) -> Result<Vec<AdminCase>> {
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::<AdminCase>(COL)
.find(query)
.limit(limit)
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|f| f.ok())
.collect()
.await)
}
}

View File

@@ -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<AdminCase> {
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<Vec<String>>,
before_id: Option<&str>,
limit: i64,
) -> Result<Vec<AdminCase>> {
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())
}
}

View File

@@ -0,0 +1,5 @@
mod models;
mod ops;
pub use models::*;
pub use ops::*;

View File

@@ -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()),
}
}
}

View File

@@ -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<AdminObjectNote>;
}

View File

@@ -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<AdminObjectNote> {
query!(self, find_one_by_id, COL, target_id)?
.ok_or_else(|| create_database_error!("find_one", COL))
}
}

View File

@@ -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(&note.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<AdminObjectNote> {
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))
}
}
}

View File

@@ -0,0 +1,5 @@
mod models;
mod ops;
pub use models::*;
pub use ops::*;

View File

@@ -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<String>,
/// Action associated with the strike (eg. suspension/ban)
pub associated_action: Option<String>,
/// 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<String>,
},
"PartialAdminStrike"
}

View File

@@ -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<AdminStrike>;
async fn admin_strike_get_user(&self, user_id: &str) -> Result<Vec<AdminStrike>>;
}

View File

@@ -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<AdminStrike> {
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<Vec<AdminStrike>> {
query!(self, find, COL, doc! {"target_id": user_id})
}
}

View File

@@ -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<AdminStrike> {
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<Vec<AdminStrike>> {
let admin_strikes = self.admin_strikes.lock().await;
Ok(admin_strikes
.iter()
.filter(|(_, strike)| strike.target_id == user_id)
.map(|(_, strike)| strike.clone())
.collect())
}
}

View File

@@ -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<Database> for AdminMachineToken {
type Rejection = Error;
async fn from_request_parts(parts: &mut Parts, _db: &Database) -> Result<AdminMachineToken> {
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))
}
}
}

View File

@@ -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::*;

View File

@@ -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()
}
}

View File

@@ -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<AdminToken>;
}

View File

@@ -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<AdminToken> {
query!(self, find_one, COL, doc! {"token": token})?
.ok_or_else(|| create_error!(InvalidCredentials))
}
}

View File

@@ -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<AdminToken> {
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))?)
}
}

View File

@@ -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<Self, Self::Error> {
let user: &Option<AdminMachineToken> = 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))
}
}
}

View File

@@ -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<Database> for AdminUser {
type Rejection = Error;
async fn from_request_parts(parts: &mut Parts, db: &Database) -> Result<AdminUser> {
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))
}
}
}

View File

@@ -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::*;

View File

@@ -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"
}

View File

@@ -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<AdminUser>;
async fn admin_user_list(&self) -> Result<Vec<AdminUser>>;
}

View File

@@ -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<AdminUser> {
query!(self, find_one_by_id, COL, user_id)?.ok_or_else(|| create_error!(NotFound))
}
async fn admin_user_list(&self) -> Result<Vec<AdminUser>> {
query!(self, find, COL, doc! {})
}
}

View File

@@ -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<AdminUser> {
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<Vec<AdminUser>> {
let admin_users = self.admin_users.lock().await;
Ok(admin_users.iter().map(|(_, u)| u).cloned().collect())
}
}

View File

@@ -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<Self, Self::Error> {
let user: &Option<AdminUser> = request
.local_cache_async(async {
let db = request.rocket().state::<Database>().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))
}
}
}

View File

@@ -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::*;
@@ -46,6 +60,13 @@ use crate::MongoDb;
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

View File

@@ -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<String>,
}
);

View File

@@ -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
}
}