feat: Implement models and meta routes. Add empty files for routes still to be implemented.

This commit is contained in:
IAmTomahawkx
2025-07-02 18:43:23 -07:00
committed by Angelo
parent 96f046bc76
commit 4fd71c98d6
100 changed files with 718 additions and 156 deletions

View File

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

View File

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

View File

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

View File

@@ -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<AdminToken>;
async fn admin_token_fetch(&self, id: &str) -> Result<AdminToken>;
}

View File

@@ -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<AdminToken> {
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
}
}

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -15,4 +15,6 @@ pub trait AbstractAdminUsers: Sync + Send {
async fn admin_user_fetch_email(&self, email: &str) -> Result<AdminUser>;
async fn admin_user_list(&self) -> Result<Vec<AdminUser>>;
async fn admin_user_count(&self) -> Result<u16>;
}

View File

@@ -28,4 +28,8 @@ impl AbstractAdminUsers for MongoDb {
async fn admin_user_list(&self) -> Result<Vec<AdminUser>> {
query!(self, find, COL, doc! {})
}
async fn admin_user_count(&self) -> Result<u16> {
query!(self, count_documents, COL, doc! {}).map(|resp| resp as u16)
}
}

View File

@@ -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<u16> {
Ok(self.admin_users.lock().await.len() as u16)
}
}

View File

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

View File

@@ -617,6 +617,7 @@ impl From<crate::Report> for Report {
additional_context: value.additional_context,
status: value.status,
notes: value.notes,
case_id: value.case_id,
}
}
}
@@ -1387,3 +1388,72 @@ impl From<FieldsMessage> for crate::FieldsMessage {
}
}
}
impl From<crate::AdminUser> 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<AdminUser> 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<AdminUserEdit> 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<AdminToken> 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<crate::AdminToken> 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<crate::AdminObjectNote> 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,
}
}
}

View File

@@ -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
@@ -25,6 +26,11 @@ impl<'a> Reference<'a> {
Reference { id }
}
/// Fetch Admin Token from Ref
pub async fn as_admin_token(&self, db: &Database) -> Result<AdminToken> {
db.admin_token_fetch(&self.id).await
}
/// Fetch ban from Ref
pub async fn as_ban(&self, db: &Database, server: &str) -> Result<ServerBan> {
db.fetch_ban(server, self.id).await