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

@@ -197,6 +197,8 @@ admin_api_enabled = false
mass_mentions_send_notifications = true
# Can role/everyone pings be used at all
mass_mentions_enabled = true
# Show the admin api in the OpenAPI spec
admin_api_show_spec = false
[features.limits]

View File

@@ -355,6 +355,7 @@ pub struct Features {
pub mass_mentions_send_notifications: bool,
pub mass_mentions_enabled: bool,
pub admin_api_enabled: bool,
pub admin_api_show_spec: bool,
#[serde(default)]
pub advanced: FeaturesAdvanced,

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

View File

@@ -1,6 +1,6 @@
use iso8601_timestamp::Timestamp;
use crate::v0::Report;
use crate::v0::{Report, User};
auto_derived! {
#[derive(Default)]
@@ -211,9 +211,6 @@ auto_derived! {
#[cfg_attr(feature = "validator", derive(validator::Validate))]
pub struct AdminTokenCreate {
/// the user who this token is for
#[cfg_attr(feature = "serde", serde(rename="user"))]
pub user_id: String,
/// The expiry timestamp for this token, in iso6801. Max 30 days from current time.
pub expiry: Timestamp,
}
@@ -229,6 +226,9 @@ auto_derived! {
pub active: bool,
/// The permissions of the user
pub permissions: u64,
/// The Revolt user attached to this user. Use this for data like names and avatars.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub revolt_user: Option<User>
}
#[cfg_attr(feature = "validator", derive(validator::Validate))]
@@ -258,4 +258,31 @@ auto_derived! {
/// The permissions of the user
pub permissions: Option<u64>,
}
#[repr(u64)]
pub enum AdminUserPermissionFlags {
ObjectNotes = 0,
ManageAdminUsers = 1,
CreateTokens = 2,
ViewUsers = 3,
ManageUsers = 4,
ManageUsersSenstiveInfo = 5,
ViewServers = 6,
ManageServers = 7,
ViewDMChannels = 8,
ManageDMChannels = 9,
ViewCases = 10,
ManageOwnCases = 11,
ManageOtherCases = 12,
ViewReports = 13,
Search = 14,
ManageNotes = 15,
Discover = 16,
}
}

View File

@@ -0,0 +1 @@
// This should allow resetting of all factors or individual ones.

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
//close/reopen a case

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,2 @@
mod actions;
mod fetch;

View File

@@ -0,0 +1,2 @@
mod actions;
mod fetch;

View File

@@ -0,0 +1,38 @@
use iso8601_timestamp::{Duration, Timestamp};
use revolt_database::{AdminMachineToken, AdminToken, Database};
use revolt_models::v0::{self};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use crate::routes::admin::util::user_has_permission;
/// Create a token for your account. Must have the CreateTokens permission
#[openapi(tag = "Admin")]
#[post("/tokens", data = "<body>")]
pub async fn admin_create_token(
db: &State<Database>,
auth: AdminMachineToken,
body: Json<v0::AdminTokenCreate>,
) -> Result<Json<v0::AdminToken>> {
if !user_has_permission(
&auth.on_behalf_of,
v0::AdminUserPermissionFlags::CreateTokens,
) {
return Err(create_error!(NotFound));
}
if body.expiry > Timestamp::now_utc() + Duration::days(30) {
return Err(create_error!(FailedValidation {
error: "The expiry is more than 30 days away.".to_string()
}));
}
let token = AdminToken::new(&auth.on_behalf_of.id, body.expiry);
db.admin_token_create(token.clone()).await?;
Ok(Json(v0::AdminToken {
id: token.id,
user_id: token.user_id,
token: token.token,
expiry: body.expiry,
}))
}

View File

@@ -0,0 +1,26 @@
use revolt_database::{AdminAuthorization, AdminUser, Database};
use revolt_models::v0::{self};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use crate::routes::admin::util::{flatten_authorized_user, user_has_permission};
/// Edit an admin user. Requires ManageAdminUsers flag.
#[openapi(tag = "Admin")]
#[post("/users", data = "<body>")]
pub async fn admin_create_user(
db: &State<Database>,
auth: AdminAuthorization,
body: Json<v0::AdminUserCreate>,
) -> Result<Json<v0::AdminUser>> {
let user = flatten_authorized_user(&auth);
if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageAdminUsers) {
return Err(create_error!(NotFound));
}
// TODO: technically there's a privilege escalation here since anyone with the manageAdminUsers permission can assign whatever permissions they want.
// But this is built with the assumption that people with ManageAdminUsers are already privileged.
let user = AdminUser::new(&body.email, &body.platform_user_id, body.permissions);
db.admin_user_insert(user.clone()).await?;
Ok(Json(user.into()))
}

View File

@@ -0,0 +1,27 @@
use revolt_database::{util::reference::Reference, AdminAuthorization, Database};
use revolt_models::v0::{self};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use rocket_empty::EmptyResponse;
use crate::routes::admin::util::{flatten_authorized_user, user_has_permission};
/// Edit an admin user. Requires ManageAdminUsers flag.
#[openapi(tag = "Admin")]
#[patch("/users/<target>", data = "<body>")]
pub async fn admin_edit_user(
db: &State<Database>,
auth: AdminAuthorization,
target: Reference,
body: Json<v0::AdminUserEdit>,
) -> Result<EmptyResponse> {
let user = flatten_authorized_user(&auth);
if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageAdminUsers) {
return Err(create_error!(NotFound));
}
// TODO: technically there's a privilege escalation here since anyone with the manageAdminUsers permission can assign whatever permissions they want.
// But this is built with the assumption that people with ManageAdminUsers are already privileged.
db.admin_user_update(&target.id, body.0.into()).await?;
Ok(EmptyResponse)
}

View File

@@ -0,0 +1,29 @@
use revolt_database::{AdminAuthorization, Database};
use revolt_models::v0::AdminUser;
use revolt_result::Result;
use rocket::{serde::json::Json, State};
/// Get a list of admin users. Any active user may use this endpoint.
/// Typically the client should cache this data.
#[openapi(tag = "Admin")]
#[get("/users")]
pub async fn admin_fetch_users(
db: &State<Database>,
auth: AdminAuthorization,
) -> Result<Json<Vec<AdminUser>>> {
let users = db.admin_user_list().await?;
let userids: Vec<String> = users.iter().map(|u| u.platform_user_id.clone()).collect();
let revolt_users = db.fetch_users(&userids).await?;
let mut resp = vec![];
for admin_user in users {
let mut user = AdminUser::from(admin_user);
user.revolt_user = match revolt_users.iter().find(|ru| ru.id == user.id) {
Some(some_user) => Some(some_user.clone().into_self(false).await),
None => None,
};
resp.push(user);
}
Ok(Json(resp))
}

View File

@@ -0,0 +1,5 @@
pub mod create_token;
pub mod create_user;
pub mod edit_user;
pub mod fetch_users;
pub mod revoke_token;

View File

@@ -0,0 +1,39 @@
use revolt_database::{util::reference::Reference, AdminMachineToken, Database};
use revolt_models::v0::{self};
use revolt_result::{create_error, Result};
use rocket::State;
use rocket_empty::EmptyResponse;
use crate::routes::admin::util::user_has_permission;
/// Revoke a token. Must be your own, or you must have the ManageAdminUsers permission to revoke other's tokens.
/// You must have the CreateTokens permission.
#[openapi(tag = "Admin")]
#[delete("/tokens/<token>")]
pub async fn admin_revoke_token(
db: &State<Database>,
auth: AdminMachineToken,
token: Reference,
) -> Result<EmptyResponse> {
if !user_has_permission(
&auth.on_behalf_of,
v0::AdminUserPermissionFlags::CreateTokens,
) {
return Err(create_error!(NotFound));
}
let token = token.as_admin_token(db).await?;
// only own user and those with ManageAdminUsers can revoke a token
if token.user_id != auth.on_behalf_of.id
&& !user_has_permission(
&auth.on_behalf_of,
v0::AdminUserPermissionFlags::ManageAdminUsers,
)
{
return Err(create_error!(NotFound));
}
db.admin_token_revoke(&token.id).await?;
Ok(EmptyResponse)
}

View File

@@ -0,0 +1,26 @@
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
use rocket::Route;
mod accounts;
mod cases;
mod meta;
mod reports;
mod roles;
mod search;
mod users;
mod object_edit_note;
mod object_get_note;
mod util;
pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![
object_edit_note::admin_object_edit_note,
object_get_note::admin_object_get_note,
meta::create_user::admin_create_user,
meta::edit_user::admin_edit_user,
meta::fetch_users::admin_fetch_users,
meta::create_token::admin_create_token,
meta::revoke_token::admin_revoke_token
]
}

View File

@@ -0,0 +1,28 @@
use revolt_database::{util::reference::Reference, AdminAuthorization, AdminObjectNote, Database};
use revolt_models::v0::{self};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use rocket_empty::EmptyResponse;
use crate::routes::admin::util::{flatten_authorized_user, user_has_permission};
/// Edit an object note. Requires ObjectNotes permission.
#[openapi(tag = "Admin")]
#[post("/notes/<object>", data = "<body>")]
pub async fn admin_object_edit_note(
db: &State<Database>,
auth: AdminAuthorization,
object: Reference,
body: Json<v0::AdminObjectNoteEdit>,
) -> Result<EmptyResponse> {
let user = flatten_authorized_user(&auth);
if !user_has_permission(user, v0::AdminUserPermissionFlags::ObjectNotes) {
return Err(create_error!(NotFound));
}
// Unfortunately due to how our system works, we can't limit users to editing objects they have permissions for (eg users, servers, etc.).
// Hence it being tied to its own permission
db.admin_note_update(AdminObjectNote::new(&object.id, &user.id, &body.content))
.await?;
Ok(EmptyResponse)
}

View File

@@ -0,0 +1,24 @@
use revolt_database::{util::reference::Reference, AdminAuthorization, Database};
use revolt_models::v0::{self};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use crate::routes::admin::util::{flatten_authorized_user, user_has_permission};
/// Edit an object note. Requires ObjectNotes permission.
#[openapi(tag = "Admin")]
#[get("/notes/<object>")]
pub async fn admin_object_get_note(
db: &State<Database>,
auth: AdminAuthorization,
object: Reference,
) -> Result<Json<v0::AdminObjectNote>> {
let user = flatten_authorized_user(&auth);
if !user_has_permission(user, v0::AdminUserPermissionFlags::ObjectNotes) {
return Err(create_error!(NotFound));
}
// Unfortunately due to how our system works, we can't limit users to editing objects they have permissions for (eg users, servers, etc.).
// Hence it being tied to its own permission
Ok(Json(db.admin_note_fetch(&object.id).await?.into()))
}

View File

@@ -0,0 +1,2 @@
mod actions;
mod fetch;

View File

@@ -0,0 +1,3 @@
mod role_create;
mod role_delete;
mod role_edit;

View File

@@ -0,0 +1 @@
mod search_everything;

View File

@@ -0,0 +1,2 @@
mod actions;
mod fetch;

View File

@@ -0,0 +1,2 @@
mod actions;
mod fetch;

View File

@@ -0,0 +1,15 @@
use revolt_database::{AdminAuthorization, AdminUser, AdminUserPermissionFlagsValue};
use revolt_models::v0::AdminUserPermissionFlags;
pub fn user_has_permission(user: &AdminUser, permission: AdminUserPermissionFlags) -> bool {
let flags = AdminUserPermissionFlagsValue(user.permissions);
flags.has(permission)
}
pub fn flatten_authorized_user<'a>(auth: &'a AdminAuthorization) -> &'a AdminUser {
match auth {
AdminAuthorization::AdminUser(admin_user) => &admin_user,
AdminAuthorization::AdminMachine(admin_machine_token) => &admin_machine_token.on_behalf_of,
}
}

View File

@@ -4,6 +4,7 @@ pub use rocket::http::Status;
pub use rocket::response::Redirect;
use rocket::{Build, Rocket};
mod admin;
mod bots;
mod channels;
mod customisation;
@@ -21,89 +22,79 @@ mod webhooks;
pub fn mount(config: Settings, mut rocket: Rocket<Build>) -> Rocket<Build> {
let settings = OpenApiSettings::default();
if config.features.webhooks_enabled {
mount_endpoints_and_merged_docs! {
rocket, "/".to_owned(), settings,
"/" => (vec![], custom_openapi_spec()),
"" => openapi_get_routes_spec![root::root],
"/users" => users::routes(),
"/bots" => bots::routes(),
"/channels" => channels::routes(),
"/servers" => servers::routes(),
"/invites" => invites::routes(),
"/custom" => customisation::routes(),
"/safety" => safety::routes(),
"/auth/account" => rocket_authifier::routes::account::routes(),
"/auth/session" => rocket_authifier::routes::session::routes(),
"/auth/mfa" => rocket_authifier::routes::mfa::routes(),
"/onboard" => onboard::routes(),
"/policy" => policy::routes(),
"/push" => push::routes(),
"/sync" => sync::routes(),
"/webhooks" => webhooks::routes()
};
} else {
mount_endpoints_and_merged_docs! {
rocket, "/".to_owned(), settings,
"/" => (vec![], custom_openapi_spec()),
"" => openapi_get_routes_spec![root::root],
"/users" => users::routes(),
"/bots" => bots::routes(),
"/channels" => channels::routes(),
"/servers" => servers::routes(),
"/invites" => invites::routes(),
"/custom" => customisation::routes(),
"/safety" => safety::routes(),
"/auth/account" => rocket_authifier::routes::account::routes(),
"/auth/session" => rocket_authifier::routes::session::routes(),
"/auth/mfa" => rocket_authifier::routes::mfa::routes(),
"/onboard" => onboard::routes(),
"/policy" => policy::routes(),
"/push" => push::routes(),
"/sync" => sync::routes()
};
let mut openapi_list: Vec<(_, revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi)> =
Vec::new();
let routes = vec![
("/", (vec![], custom_openapi_spec()), true, true),
("/", openapi_get_routes_spec![root::root], true, true),
("/users", users::routes(), true, true),
("/bots", bots::routes(), true, true),
("/channels", channels::routes(), true, true),
("/servers", servers::routes(), true, true),
("/invites", invites::routes(), true, true),
("/custom", customisation::routes(), true, true),
("/safety", safety::routes(), true, true),
(
"/auth/mfa",
rocket_authifier::routes::mfa::routes(),
true,
true,
),
("/onboard", onboard::routes(), true, true),
("/policy", policy::routes(), true, true),
("/push", push::routes(), true, true),
("/sync", sync::routes(), true, true),
(
"/auth/account",
rocket_authifier::routes::account::routes(),
true,
true,
),
(
"/auth/session",
rocket_authifier::routes::session::routes(),
true,
true,
),
(
"/admin",
admin::routes(),
config.features.admin_api_enabled,
config.features.admin_api_show_spec,
),
(
"/webhooks",
webhooks::routes(),
config.features.webhooks_enabled,
true,
),
];
for (base, (routes, openapi), enabled, show_spec) in routes {
if !enabled {
continue;
}
rocket = rocket.mount(base, routes);
if show_spec {
openapi_list.push((base, openapi));
}
}
if config.features.webhooks_enabled {
mount_endpoints_and_merged_docs! {
rocket, "/0.8".to_owned(), settings,
"/" => (vec![], custom_openapi_spec()),
"" => openapi_get_routes_spec![root::root],
"/users" => users::routes(),
"/bots" => bots::routes(),
"/channels" => channels::routes(),
"/servers" => servers::routes(),
"/invites" => invites::routes(),
"/custom" => customisation::routes(),
"/safety" => safety::routes(),
"/auth/account" => rocket_authifier::routes::account::routes(),
"/auth/session" => rocket_authifier::routes::session::routes(),
"/auth/mfa" => rocket_authifier::routes::mfa::routes(),
"/onboard" => onboard::routes(),
"/push" => push::routes(),
"/sync" => sync::routes(),
"/webhooks" => webhooks::routes()
let openapi_docs =
match revolt_rocket_okapi::revolt_okapi::merge::marge_spec_list(&openapi_list) {
Ok(docs) => docs,
Err(err) => panic!("Could not merge OpenAPI spec: {}", err),
};
} else {
mount_endpoints_and_merged_docs! {
rocket, "/0.8".to_owned(), settings,
"/" => (vec![], custom_openapi_spec()),
"" => openapi_get_routes_spec![root::root],
"/users" => users::routes(),
"/bots" => bots::routes(),
"/channels" => channels::routes(),
"/servers" => servers::routes(),
"/invites" => invites::routes(),
"/custom" => customisation::routes(),
"/safety" => safety::routes(),
"/auth/account" => rocket_authifier::routes::account::routes(),
"/auth/session" => rocket_authifier::routes::session::routes(),
"/auth/mfa" => rocket_authifier::routes::mfa::routes(),
"/onboard" => onboard::routes(),
"/push" => push::routes(),
"/sync" => sync::routes()
};
}
rocket = rocket.mount(
"/",
vec![revolt_rocket_okapi::get_openapi_route(
openapi_docs,
&settings,
)],
);
rocket
}
@@ -238,11 +229,6 @@ fn custom_openapi_spec() -> OpenApi {
description: Some("Local Revolt Environment".to_owned()),
..Default::default()
},
Server {
url: "http://local.revolt.chat:14702/0.8".to_owned(),
description: Some("Local Revolt Environment (v0.8)".to_owned()),
..Default::default()
},
],
external_docs: Some(ExternalDocs {
url: "https://developers.revolt.chat".to_owned(),
@@ -251,6 +237,13 @@ fn custom_openapi_spec() -> OpenApi {
}),
extensions,
tags: vec![
Tag {
name: "Admin".to_owned(),
description: Some(
"Used when running your own instance to manage and moderate".to_owned(),
),
..Default::default()
},
Tag {
name: "Core".to_owned(),
description: Some(

View File

@@ -123,6 +123,7 @@ pub async fn report_content(
additional_context: data.additional_context,
status: ReportStatus::Created {},
notes: String::new(),
case_id: None,
};
db.insert_report(&report).await?;