mirror of
https://github.com/stoatchat/stoatchat.git
synced 2026-07-14 13:36:59 +00:00
feat: Implement models and meta routes. Add empty files for routes still to be implemented.
This commit is contained in:
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
59
crates/core/database/src/models/admin_tokens/schema.rs
Normal file
59
crates/core/database/src/models/admin_tokens/schema.rs
Normal 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,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user