From 897e4b9a6522e7711e46a8033006112597ceab4b Mon Sep 17 00:00:00 2001 From: ValDesign Date: Fri, 17 Apr 2026 16:47:02 +0200 Subject: [PATCH 1/9] fix(routes): bot auth now fixed --- src/api/routes/bots/bot/achievements/mod.rs | 16 +++--- src/api/routes/bots/bot/events/event/mod.rs | 54 ++++++++++++--------- src/api/routes/bots/bot/events/mod.rs | 32 ++++++------ src/api/routes/bots/bot/mod.rs | 34 +++++++------ src/api/routes/bots/bot/settings/mod.rs | 16 +++--- src/api/routes/bots/bot/stats/mod.rs | 32 ++++++------ src/api/routes/bots/bot/team/mod.rs | 52 +++++++++++--------- src/api/routes/bots/bot/token/mod.rs | 32 ++++++------ 8 files changed, 149 insertions(+), 119 deletions(-) diff --git a/src/api/routes/bots/bot/achievements/mod.rs b/src/api/routes/bots/bot/achievements/mod.rs index b39477b5..1d687da5 100644 --- a/src/api/routes/bots/bot/achievements/mod.rs +++ b/src/api/routes/bots/bot/achievements/mod.rs @@ -56,13 +56,15 @@ async fn get_bot_achievements( bot_id = %bot_id, "Admin access granted for bot achievements", ); - } else if ctx.is_bot() && ctx.token.as_deref() != Some(&bot.token) { - warn!( - code = %LogCode::Forbidden, - bot_id = %bot_id, - "Bot attempting to access achievements of another bot", - ); - return Err(ApiError::Forbidden); + } else if ctx.is_bot() { + if ctx.token.as_deref() != Some(&bot.token) { + warn!( + code = %LogCode::Forbidden, + bot_id = %bot_id, + "Bot attempting to access achievements of another bot", + ); + return Err(ApiError::Forbidden); + } } else if ctx.is_user() { let user_id = ctx.user_id.as_deref().ok_or(ApiError::Unauthorized)?; if !bot.has_access(user_id) { diff --git a/src/api/routes/bots/bot/events/event/mod.rs b/src/api/routes/bots/bot/events/event/mod.rs index 3bc2659f..50f60c06 100644 --- a/src/api/routes/bots/bot/events/event/mod.rs +++ b/src/api/routes/bots/bot/events/event/mod.rs @@ -44,14 +44,16 @@ async fn get_event( event_key = %event_key, "Admin access granted for retrieving custom event", ); - } else if ctx.is_bot() && ctx.bot_id.as_deref() != Some(&bot_id) { - warn!( - code = %LogCode::Forbidden, - bot_id = %bot_id, - event_key = %event_key, - "Bot access denied for retrieving custom event", - ); - return Err(ApiError::Forbidden); + } else if ctx.is_bot() { + if ctx.token.as_deref() != Some(&bot.token) { + warn!( + code = %LogCode::Forbidden, + bot_id = %bot_id, + event_key = %event_key, + "Bot access denied for retrieving custom event", + ); + return Err(ApiError::Forbidden); + } } else if ctx.is_user() { let user_id = ctx.user_id.as_deref().ok_or(ApiError::Unauthorized)?; if !bot.has_access(user_id) { @@ -135,14 +137,16 @@ async fn update_event( event_key = %event_key, "Admin access granted for updating custom event", ); - } else if ctx.is_bot() && ctx.bot_id.as_deref() != Some(&bot_id) { - warn!( - code = %LogCode::Forbidden, - bot_id = %bot_id, - event_key = %event_key, - "Bot access denied for updating custom event", - ); - return Err(ApiError::Forbidden); + } else if ctx.is_bot() { + if ctx.token.as_deref() != Some(&bot.token) { + warn!( + code = %LogCode::Forbidden, + bot_id = %bot_id, + event_key = %event_key, + "Bot access denied for updating custom event", + ); + return Err(ApiError::Forbidden); + } } else if ctx.is_user() { let user_id = ctx.user_id.as_deref().ok_or(ApiError::Unauthorized)?; if !bot.has_access(user_id) { @@ -253,14 +257,16 @@ async fn delete_event( event_key = %event_key, "Admin access granted for deleting custom event", ); - } else if ctx.is_bot() && ctx.bot_id.as_deref() != Some(&bot_id) { - warn!( - code = %LogCode::Forbidden, - bot_id = %bot_id, - event_key = %event_key, - "Bot access denied for deleting custom event", - ); - return Err(ApiError::Forbidden); + } else if ctx.is_bot() { + if ctx.token.as_deref() != Some(&bot.token) { + warn!( + code = %LogCode::Forbidden, + bot_id = %bot_id, + event_key = %event_key, + "Bot access denied for deleting custom event", + ); + return Err(ApiError::Forbidden); + } } else if ctx.is_user() { let user_id = ctx.user_id.as_deref().ok_or(ApiError::Unauthorized)?; if !bot.has_access(user_id) { diff --git a/src/api/routes/bots/bot/events/mod.rs b/src/api/routes/bots/bot/events/mod.rs index c4e89613..be692069 100644 --- a/src/api/routes/bots/bot/events/mod.rs +++ b/src/api/routes/bots/bot/events/mod.rs @@ -53,13 +53,15 @@ async fn get_all_events( bot_id = %bot_id, "Admin access granted for retrieving all custom events", ); - } else if ctx.is_bot() && ctx.bot_id.as_deref() != Some(&bot_id) { - warn!( - code = %LogCode::Forbidden, - bot_id = %bot_id, - "Bot attempting to retrieve custom events for a different bot", - ); - return Err(ApiError::Forbidden); + } else if ctx.is_bot() { + if ctx.token.as_deref() != Some(&bot.token) { + warn!( + code = %LogCode::Forbidden, + bot_id = %bot_id, + "Bot attempting to retrieve custom events for a different bot", + ); + return Err(ApiError::Forbidden); + } } else if ctx.is_user() { let user_id = ctx.user_id.as_deref().ok_or(ApiError::Unauthorized)?; if !bot.has_access(user_id) { @@ -132,13 +134,15 @@ async fn create_event( bot_id = %bot_id, "Admin access granted for creating custom event", ); - } else if ctx.is_bot() && ctx.bot_id.as_deref() != Some(&bot_id) { - warn!( - code = %LogCode::Forbidden, - bot_id = %bot_id, - "Bot attempting to create custom event for a different bot", - ); - return Err(ApiError::Forbidden); + } else if ctx.is_bot() { + if ctx.token.as_deref() != Some(&bot.token) { + warn!( + code = %LogCode::Forbidden, + bot_id = %bot_id, + "Bot attempting to create custom event for a different bot", + ); + return Err(ApiError::Forbidden); + } } else if ctx.is_user() { let user_id = ctx.user_id.as_deref().ok_or(ApiError::Unauthorized)?; if !bot.has_access(user_id) { diff --git a/src/api/routes/bots/bot/mod.rs b/src/api/routes/bots/bot/mod.rs index 7d2cd713..68071d6d 100644 --- a/src/api/routes/bots/bot/mod.rs +++ b/src/api/routes/bots/bot/mod.rs @@ -71,13 +71,15 @@ async fn get_bot( bot_id = %bot_id, "Admin access granted for bot details", ); - } else if ctx.is_bot() && ctx.token.as_deref() != Some(&bot.token) { - warn!( - code = %LogCode::Forbidden, - bot_id = %bot_id, - "Bot attempting to access details of another bot", - ); - return Err(ApiError::Forbidden); + } else if ctx.is_bot() { + if ctx.token.as_deref() != Some(&bot.token) { + warn!( + code = %LogCode::Forbidden, + bot_id = %bot_id, + "Bot attempting to access details of another bot", + ); + return Err(ApiError::Forbidden); + } } else if ctx.is_user() { let user_id = ctx.user_id.as_deref().ok_or(ApiError::Unauthorized)?; if !bot.has_access(user_id) { @@ -245,6 +247,15 @@ async fn patch_bot( "Attempting to update bot", ); + let bot = repos.bots.find_by_id(&bot_id).await?.ok_or_else(|| { + info!( + code = %LogCode::Request, + bot_id = %bot_id, + "Bot not found for update", + ); + ApiError::NotFound(format!("Bot with ID {} not found", bot_id)) + })?; + let ctx = &auth; if !(ctx.is_admin() || ctx.is_bot() && ctx.bot_id.as_deref() == Some(bot_id.as_str())) { @@ -257,15 +268,6 @@ async fn patch_bot( return Err(ApiError::Forbidden); } - let bot = repos.bots.find_by_id(&bot_id).await?.ok_or_else(|| { - info!( - code = %LogCode::Request, - bot_id = %bot_id, - "Bot not found for update", - ); - ApiError::NotFound(format!("Bot with ID {} not found", bot_id)) - })?; - if bot.suspended && !ctx.is_admin() { warn!( code = %LogCode::Forbidden, diff --git a/src/api/routes/bots/bot/settings/mod.rs b/src/api/routes/bots/bot/settings/mod.rs index 58978cf6..566508fe 100644 --- a/src/api/routes/bots/bot/settings/mod.rs +++ b/src/api/routes/bots/bot/settings/mod.rs @@ -49,13 +49,15 @@ async fn update_settings( bot_id = %bot_id, "User is admin, proceeding with settings update", ); - } else if ctx.is_bot() && ctx.token.as_deref() != Some(&bot.token) { - warn!( - code = %LogCode::Forbidden, - bot_id = %bot_id, - "Bot attempted to update settings of a different bot", - ); - return Err(ApiError::Forbidden); + } else if ctx.is_bot() { + if ctx.token.as_deref() != Some(&bot.token) { + warn!( + code = %LogCode::Forbidden, + bot_id = %bot_id, + "Bot attempted to update settings of a different bot", + ); + return Err(ApiError::Forbidden); + } } else if ctx.is_user() { let user_id = ctx.user_id.as_deref().ok_or(ApiError::Unauthorized)?; if !bot.is_owner(user_id) { diff --git a/src/api/routes/bots/bot/stats/mod.rs b/src/api/routes/bots/bot/stats/mod.rs index 87f4c43e..3d3462e5 100644 --- a/src/api/routes/bots/bot/stats/mod.rs +++ b/src/api/routes/bots/bot/stats/mod.rs @@ -110,13 +110,15 @@ async fn get_stats( bot_id = %bot_id, "Admin access granted for bot stats", ); - } else if ctx.is_bot() && ctx.bot_id.as_deref() != Some(&bot_id) { - warn!( - code = %LogCode::Forbidden, - bot_id = %bot_id, - "Bot attempting to access stats of another bot", - ); - return Err(ApiError::Forbidden); + } else if ctx.is_bot() { + if ctx.token.as_deref() != Some(&bot.token) { + warn!( + code = %LogCode::Forbidden, + bot_id = %bot_id, + "Bot attempting to access stats of another bot", + ); + return Err(ApiError::Forbidden); + } } else if ctx.is_user() { let user_id = ctx.user_id.as_deref().ok_or(ApiError::Unauthorized)?; if !bot.has_access(user_id) { @@ -214,13 +216,15 @@ async fn post_stats( bot_id = %bot_id, "Admin access granted for posting bot stats", ); - } else if ctx.is_bot() && ctx.bot_id.as_deref() != Some(&bot_id) { - warn!( - code = %LogCode::Forbidden, - bot_id = %bot_id, - "Bot attempting to post stats for another bot", - ); - return Err(ApiError::Forbidden); + } else if ctx.is_bot() { + if ctx.token.as_deref() != Some(&bot.token) { + warn!( + code = %LogCode::Forbidden, + bot_id = %bot_id, + "Bot attempting to post stats for another bot", + ); + return Err(ApiError::Forbidden); + } } else if !ctx.is_admin() && !ctx.is_bot() { warn!( code = %LogCode::Forbidden, diff --git a/src/api/routes/bots/bot/team/mod.rs b/src/api/routes/bots/bot/team/mod.rs index a3028e75..d62510ed 100644 --- a/src/api/routes/bots/bot/team/mod.rs +++ b/src/api/routes/bots/bot/team/mod.rs @@ -57,13 +57,15 @@ async fn get_team( bot_id = %bot_id, "Admin access granted to fetch team for bot", ); - } else if ctx.is_bot() && ctx.bot_id.as_deref() != Some(&bot_id) { - warn!( - code = %LogCode::Forbidden, - bot_id = %bot_id, - "Bot access denied to fetch team for another bot", - ); - return Err(ApiError::Forbidden); + } else if ctx.is_bot() { + if ctx.token.as_deref() != Some(&bot.token) { + warn!( + code = %LogCode::Forbidden, + bot_id = %bot_id, + "Bot access denied to fetch team for another bot", + ); + return Err(ApiError::Forbidden); + } } else if ctx.is_user() { let user_id = ctx.user_id.as_deref().ok_or(ApiError::Unauthorized)?; if !bot.is_owner(user_id) { @@ -172,14 +174,16 @@ async fn add_to_team( user_id = %body.user_id, "Admin access granted to add user to bot team", ); - } else if ctx.is_bot() && ctx.bot_id.as_deref() != Some(&bot_id) { - warn!( - code = %LogCode::Forbidden, - bot_id = %bot_id, - user_id = %body.user_id, - "Bot access denied to add user to another bot team", - ); - return Err(ApiError::Forbidden); + } else if ctx.is_bot() { + if ctx.token.as_deref() != Some(&bot.token) { + warn!( + code = %LogCode::Forbidden, + bot_id = %bot_id, + user_id = %body.user_id, + "Bot access denied to add user to another bot team", + ); + return Err(ApiError::Forbidden); + } } else if ctx.is_user() { let user_id = ctx.user_id.as_deref().ok_or(ApiError::Unauthorized)?; if !bot.is_owner(user_id) { @@ -382,14 +386,16 @@ async fn delete_from_team( user_id = %body.user_id, "Admin access granted to remove user from bot team", ); - } else if ctx.is_bot() && ctx.bot_id.as_deref() != Some(&bot_id) { - warn!( - code = %LogCode::Forbidden, - bot_id = %bot_id, - user_id = %body.user_id, - "Bot access denied to remove user from another bot team", - ); - return Err(ApiError::Forbidden); + } else if ctx.is_bot() { + if ctx.token.as_deref() != Some(&bot.token) { + warn!( + code = %LogCode::Forbidden, + bot_id = %bot_id, + user_id = %body.user_id, + "Bot access denied to remove user from another bot team", + ); + return Err(ApiError::Forbidden); + } } else if ctx.is_user() { let user_id = ctx.user_id.as_deref().ok_or(ApiError::Unauthorized)?; if !bot.is_owner(user_id) { diff --git a/src/api/routes/bots/bot/token/mod.rs b/src/api/routes/bots/bot/token/mod.rs index 8d78232e..401baaea 100644 --- a/src/api/routes/bots/bot/token/mod.rs +++ b/src/api/routes/bots/bot/token/mod.rs @@ -55,13 +55,15 @@ async fn get_token( bot_id = %bot_id, "Admin access granted for token retrieval", ); - } else if ctx.is_bot() && ctx.bot_id.as_deref() != Some(&bot_id) { - warn!( - code = %LogCode::Forbidden, - bot_id = %bot_id, - "Bot attempting to retrieve token for a different bot", - ); - return Err(ApiError::Forbidden); + } else if ctx.is_bot() { + if ctx.token.as_deref() != Some(&bot.token) { + warn!( + code = %LogCode::Forbidden, + bot_id = %bot_id, + "Bot attempting to retrieve token for a different bot", + ); + return Err(ApiError::Forbidden); + } } else if ctx.is_user() { let user_id = ctx.user_id.as_deref().ok_or(ApiError::Unauthorized)?; if !bot.is_owner(user_id) { @@ -127,13 +129,15 @@ async fn refresh_token( bot_id = %bot_id, "Admin access granted for token refresh", ); - } else if ctx.is_bot() && ctx.bot_id.as_deref() != Some(&bot_id) { - warn!( - code = %LogCode::Forbidden, - bot_id = %bot_id, - "Bot attempting to refresh token for a different bot", - ); - return Err(ApiError::Forbidden); + } else if ctx.is_bot() { + if ctx.token.as_deref() != Some(&bot.token) { + warn!( + code = %LogCode::Forbidden, + bot_id = %bot_id, + "Bot attempting to refresh token for a different bot", + ); + return Err(ApiError::Forbidden); + } } else if ctx.is_user() { let user_id = ctx.user_id.as_deref().ok_or(ApiError::Unauthorized)?; if !bot.is_owner(user_id) { From 7c347e42a271ef93b4a00baf9cd74490a6bde73c Mon Sep 17 00:00:00 2001 From: ValDesign Date: Fri, 17 Apr 2026 17:12:20 +0200 Subject: [PATCH 2/9] fix(routes): add missing field to custom event response --- src/api/routes/bots/bot/events/event/mod.rs | 23 ++++++++--- src/api/routes/bots/bot/events/mod.rs | 10 ++--- src/api/routes/bots/bot/stats/mod.rs | 17 +++------ src/openapi/schemas/bot_stat.rs | 42 ++------------------- src/openapi/schemas/custom_event.rs | 26 ++++++++++++- src/openapi/schemas/mod.rs | 6 +-- 6 files changed, 60 insertions(+), 64 deletions(-) diff --git a/src/api/routes/bots/bot/events/event/mod.rs b/src/api/routes/bots/bot/events/event/mod.rs index 50f60c06..f99465c3 100644 --- a/src/api/routes/bots/bot/events/event/mod.rs +++ b/src/api/routes/bots/bot/events/event/mod.rs @@ -3,12 +3,13 @@ use apistos::{ api_operation, web::{ServiceConfig, delete, get, patch, resource, scope}, }; +use mongodb::bson::DateTime; use tracing::{error, info, warn}; use crate::{ api::middleware::Authenticated, domain::error::{ApiError, ApiResult}, - openapi::schemas::{CustomEventPayload, CustomEventUpdatePayload, MessageResponse}, + openapi::schemas::{CustomEventResponse, CustomEventUpdatePayload, MessageResponse}, repository::{CustomEventUpdate, Repositories}, utils::{discord::Snowflake, logger::LogCode}, }; @@ -22,7 +23,7 @@ async fn get_event( auth: Authenticated, repos: Data, path: Path<(String, String)>, -) -> ApiResult> { +) -> ApiResult> { let (id, event_key) = path.into_inner(); let bot_id = Snowflake::try_from(id)?.into_inner(); @@ -102,7 +103,19 @@ async fn get_event( )) })?; - Ok(Json(CustomEventPayload::from(event))) + let current_date = DateTime::now(); + let start_of_hour = DateTime::from_millis( + current_date.timestamp_millis() - (current_date.timestamp_millis() % 3600000), + ); + + let stats = repos + .bot_stats + .find_by_date(&bot_id, &start_of_hour) + .await?; + + let current_value = stats.and_then(|s| s.custom_events.get(&event_key).copied()); + + Ok(Json(CustomEventResponse::new(event, current_value))) } #[api_operation( @@ -115,7 +128,7 @@ async fn update_event( repos: Data, body: Json, path: Path<(String, String)>, -) -> ApiResult> { +) -> ApiResult> { let (id, event_key) = path.into_inner(); let bot_id = Snowflake::try_from(id)?.into_inner(); @@ -223,7 +236,7 @@ async fn update_event( "Custom event updated successfully", ); - Ok(Json(CustomEventPayload::from(update_result))) + Ok(Json(CustomEventResponse::from(update_result))) } #[api_operation( diff --git a/src/api/routes/bots/bot/events/mod.rs b/src/api/routes/bots/bot/events/mod.rs index be692069..731cfaaa 100644 --- a/src/api/routes/bots/bot/events/mod.rs +++ b/src/api/routes/bots/bot/events/mod.rs @@ -13,7 +13,7 @@ use crate::{ error::{ApiError, ApiResult}, models::CustomEvent, }, - openapi::schemas::CustomEventPayload, + openapi::schemas::{CustomEventPayload, CustomEventResponse}, repository::Repositories, utils::{discord::Snowflake, logger::LogCode}, }; @@ -27,7 +27,7 @@ async fn get_all_events( auth: Authenticated, repos: Data, id: Path, -) -> ApiResult>> { +) -> ApiResult>> { let bot_id = Snowflake::try_from(id.into_inner())?.into_inner(); info!( @@ -93,7 +93,7 @@ async fn get_all_events( let events = repos.custom_events.find_by_bot_id(&bot_id).await?; - let event_responses = events.into_iter().map(CustomEventPayload::from).collect(); + let event_responses = events.into_iter().map(CustomEventResponse::from).collect(); info!( code = %LogCode::Request, @@ -114,7 +114,7 @@ async fn create_event( repos: Data, event: Json, id: Path, -) -> ApiResult> { +) -> ApiResult> { let bot_id = Snowflake::try_from(id.into_inner())?.into_inner(); let bot = repos.bots.find_by_id(&bot_id).await?.ok_or_else(|| { @@ -217,7 +217,7 @@ async fn create_event( "Custom event created successfully", ); - Ok(Json(CustomEventPayload::from(new_event))) + Ok(Json(CustomEventResponse::from(new_event))) } pub fn configure(cfg: &mut ServiceConfig) { diff --git a/src/api/routes/bots/bot/stats/mod.rs b/src/api/routes/bots/bot/stats/mod.rs index 3d3462e5..dabe4f5c 100644 --- a/src/api/routes/bots/bot/stats/mod.rs +++ b/src/api/routes/bots/bot/stats/mod.rs @@ -13,11 +13,11 @@ use crate::{ api::middleware::Authenticated, domain::{ error::{ApiError, ApiResult}, - models::AchievementType, + models::{AchievementType, BotStats}, }, openapi::schemas::{ BotStatsBody, BotStatsContent, BotStatsQuery, BotStatsResponse, MessageResponse, - NormalizedStatsBody, VoteResponse, + VoteResponse, }, repository::{BotStatsUpdate, BotUpdate, Repositories}, utils::{constants::MAX_DATE_RANGE, discord::Snowflake, logger::LogCode}, @@ -249,12 +249,8 @@ async fn post_stats( ); let body = match body.into_inner() { - BotStatsBody::New(new_body) => { - NormalizedStatsBody::from_new(new_body, &bot_id, &start_of_hour) - } - BotStatsBody::Old(old_body) => { - NormalizedStatsBody::from_old(old_body, &bot_id, &start_of_hour) - } + BotStatsBody::New(new_body) => BotStats::from_new(new_body, &bot_id, &start_of_hour), + BotStatsBody::Old(old_body) => BotStats::from_old(old_body, &bot_id, &start_of_hour), }; let new_stats = match repos @@ -355,9 +351,8 @@ async fn post_stats( })? } None => { - let new_stats = body.into_stats(); - repos.bot_stats.insert(&new_stats).await?; - new_stats + repos.bot_stats.insert(&body).await?; + body } }; diff --git a/src/openapi/schemas/bot_stat.rs b/src/openapi/schemas/bot_stat.rs index 686cc27b..9dbeb722 100644 --- a/src/openapi/schemas/bot_stat.rs +++ b/src/openapi/schemas/bot_stat.rs @@ -70,6 +70,7 @@ pub enum BotStatsBody { } #[derive(Deserialize, Serialize, Clone, ApiComponent, JsonSchema)] +#[deprecated] pub struct OldInteraction { pub command_type: Option, pub name: String, @@ -90,6 +91,7 @@ impl From for Interaction { } #[derive(Deserialize, Serialize, Clone, ApiComponent, JsonSchema)] +#[deprecated] pub struct OldUserType { pub admin: i32, pub moderator: i32, @@ -111,6 +113,7 @@ impl From for UserType { } #[derive(Deserialize, Serialize, Clone, ApiComponent, JsonSchema)] +#[deprecated] pub struct BotStatsBodyOld { #[serde(rename = "addedGuilds")] pub added_guilds: i32, @@ -148,25 +151,7 @@ pub struct BotStatsBodyNew { pub users_type: Option, } -#[derive(Clone, Debug)] -pub struct NormalizedStatsBody { - pub added_guilds: i32, - pub bot_id: String, - pub custom_events: HashMap, - pub date: DateTime, - pub guilds: Option>, - pub guild_count: i32, - pub guild_locales: Vec, - pub guild_members: GuildMembers, - pub interactions: Vec, - pub interactions_locales: Vec, - pub removed_guilds: i32, - pub user_count: i32, - pub user_install_count: Option, - pub users_type: Option, -} - -impl NormalizedStatsBody { +impl BotStats { pub fn from_old(old: BotStatsBodyOld, bot_id: &str, date: &DateTime) -> Self { Self { added_guilds: old.added_guilds, @@ -204,23 +189,4 @@ impl NormalizedStatsBody { users_type: new.users_type, } } - - pub fn into_stats(self) -> BotStats { - BotStats { - added_guilds: self.added_guilds, - bot_id: self.bot_id, - custom_events: self.custom_events, - date: self.date, - guilds: self.guilds, - guild_count: self.guild_count, - guild_locales: self.guild_locales, - guild_members: self.guild_members, - interactions: self.interactions, - interactions_locales: self.interactions_locales, - removed_guilds: self.removed_guilds, - user_count: self.user_count, - user_install_count: self.user_install_count, - users_type: self.users_type, - } - } } diff --git a/src/openapi/schemas/custom_event.rs b/src/openapi/schemas/custom_event.rs index a61546ee..f1dcb610 100644 --- a/src/openapi/schemas/custom_event.rs +++ b/src/openapi/schemas/custom_event.rs @@ -12,12 +12,36 @@ pub struct CustomEventPayload { pub graph_name: String, } -impl From for CustomEventPayload { +#[derive(Deserialize, Serialize, Clone, ApiComponent, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct CustomEventResponse { + pub current_value: Option, + pub default_value: Option, + pub event_key: String, + pub graph_name: String, + #[deprecated] + #[serde(rename = "today_value")] + pub today_value: Option, +} + +impl CustomEventResponse { + pub fn new(event: CustomEvent, current_value: Option) -> Self { + Self { + current_value, + today_value: current_value, + ..Self::from(event) + } + } +} + +impl From for CustomEventResponse { fn from(event: CustomEvent) -> Self { Self { + current_value: None, default_value: event.default_value, event_key: event.event_key, graph_name: event.graph_name, + today_value: None, } } } diff --git a/src/openapi/schemas/mod.rs b/src/openapi/schemas/mod.rs index 5b935d44..f3b939d6 100644 --- a/src/openapi/schemas/mod.rs +++ b/src/openapi/schemas/mod.rs @@ -31,10 +31,8 @@ pub use bot::{ BotCreationBody, BotDeletionPayload, BotResponse, BotSettingsPayload, BotSuspendRequest, BotTokenResponse, BotUpdateBody, }; -pub use bot_stat::{ - BotStatsBody, BotStatsContent, BotStatsQuery, BotStatsResponse, NormalizedStatsBody, -}; -pub use custom_event::{CustomEventPayload, CustomEventUpdatePayload}; +pub use bot_stat::{BotStatsBody, BotStatsContent, BotStatsQuery, BotStatsResponse}; +pub use custom_event::{CustomEventPayload, CustomEventResponse, CustomEventUpdatePayload}; pub use health::HealthResponse; pub use integrations::{IntegrationPayload, TopGGIntegrationPayload}; pub use invitation::{InvitationAcceptBody, InvitationAcceptResponse, InvitationResponse}; From cffe4e38093248ec540355823e17aa93ec500e56 Mon Sep 17 00:00:00 2001 From: ValDesign Date: Fri, 17 Apr 2026 17:52:40 +0200 Subject: [PATCH 3/9] fix(ci): works with deprecated structs and fields now --- src/openapi/schemas/bot_stat.rs | 2 ++ src/openapi/schemas/custom_event.rs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/openapi/schemas/bot_stat.rs b/src/openapi/schemas/bot_stat.rs index 9dbeb722..eccd1b5d 100644 --- a/src/openapi/schemas/bot_stat.rs +++ b/src/openapi/schemas/bot_stat.rs @@ -1,3 +1,5 @@ +#![allow(deprecated)] + use std::collections::HashMap; use apistos::ApiComponent; diff --git a/src/openapi/schemas/custom_event.rs b/src/openapi/schemas/custom_event.rs index f1dcb610..b770d011 100644 --- a/src/openapi/schemas/custom_event.rs +++ b/src/openapi/schemas/custom_event.rs @@ -1,3 +1,5 @@ +#![allow(deprecated)] + use apistos::ApiComponent; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; From 4fcbd960b8c68d5f77cf436f357b4316a710c23c Mon Sep 17 00:00:00 2001 From: ValDesign Date: Fri, 17 Apr 2026 20:26:56 +0200 Subject: [PATCH 4/9] fix(routes): current_value should be i32 instead of None now --- src/api/routes/bots/bot/events/event/mod.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/api/routes/bots/bot/events/event/mod.rs b/src/api/routes/bots/bot/events/event/mod.rs index f99465c3..b8e6e310 100644 --- a/src/api/routes/bots/bot/events/event/mod.rs +++ b/src/api/routes/bots/bot/events/event/mod.rs @@ -104,16 +104,18 @@ async fn get_event( })?; let current_date = DateTime::now(); - let start_of_hour = DateTime::from_millis( - current_date.timestamp_millis() - (current_date.timestamp_millis() % 3600000), + let start_of_day = DateTime::from_millis( + current_date.timestamp_millis() - (current_date.timestamp_millis() % 86400000), ); let stats = repos .bot_stats - .find_by_date(&bot_id, &start_of_hour) + .find_from_date_range(&bot_id, &start_of_day, ¤t_date) .await?; - let current_value = stats.and_then(|s| s.custom_events.get(&event_key).copied()); + let current_value = stats + .last() + .and_then(|s| s.custom_events.get(&event_key).copied()); Ok(Json(CustomEventResponse::new(event, current_value))) } From 0cc3b421b0c210363962603b93ec75ce22f50b84 Mon Sep 17 00:00:00 2001 From: ValDesign Date: Sat, 18 Apr 2026 11:37:30 +0200 Subject: [PATCH 5/9] fix(routes): custom_event current_value is default_value if None --- src/api/routes/bots/bot/events/event/mod.rs | 16 +++------------- src/repository/bot_stats.rs | 10 +++++++++- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/api/routes/bots/bot/events/event/mod.rs b/src/api/routes/bots/bot/events/event/mod.rs index b8e6e310..77c51e47 100644 --- a/src/api/routes/bots/bot/events/event/mod.rs +++ b/src/api/routes/bots/bot/events/event/mod.rs @@ -3,7 +3,6 @@ use apistos::{ api_operation, web::{ServiceConfig, delete, get, patch, resource, scope}, }; -use mongodb::bson::DateTime; use tracing::{error, info, warn}; use crate::{ @@ -103,19 +102,10 @@ async fn get_event( )) })?; - let current_date = DateTime::now(); - let start_of_day = DateTime::from_millis( - current_date.timestamp_millis() - (current_date.timestamp_millis() % 86400000), - ); - - let stats = repos - .bot_stats - .find_from_date_range(&bot_id, &start_of_day, ¤t_date) - .await?; - + let stats = repos.bot_stats.find_last(&bot_id).await?; let current_value = stats - .last() - .and_then(|s| s.custom_events.get(&event_key).copied()); + .and_then(|s| s.custom_events.get(&event_key).copied()) + .or(event.default_value); Ok(Json(CustomEventResponse::new(event, current_value))) } diff --git a/src/repository/bot_stats.rs b/src/repository/bot_stats.rs index 27810033..32b07b0e 100644 --- a/src/repository/bot_stats.rs +++ b/src/repository/bot_stats.rs @@ -5,7 +5,7 @@ use mongodb::{ Collection, Database, bson::{DateTime, Document, doc}, error::Result, - options::{FindOneAndUpdateOptions, FindOptions, ReturnDocument}, + options::{FindOneAndUpdateOptions, FindOneOptions, FindOptions, ReturnDocument}, results::{DeleteResult, InsertOneResult}, }; @@ -208,6 +208,14 @@ impl BotStatsRepository { }) } + pub async fn find_last(&self, bot_id: &str) -> Result> { + let options = FindOneOptions::builder().sort(doc! { "date": -1 }).build(); + self.collection + .find_one(doc! { "botId": bot_id }) + .with_options(options) + .await + } + pub async fn find_by_date(&self, bot_id: &str, date: &DateTime) -> Result> { self.collection .find_one(doc! { "botId": bot_id, "date": date }) From 59c00a1e48da2363e34d3a758ee6d18bb9a4c4fd Mon Sep 17 00:00:00 2001 From: ValDesign Date: Sat, 18 Apr 2026 11:57:36 +0200 Subject: [PATCH 6/9] chore(auth): cleaned up auth checks --- src/api/routes/bots/bot/achievements/mod.rs | 6 +-- src/api/routes/bots/bot/mod.rs | 29 ++++++------ src/api/routes/bots/bot/reports/mod.rs | 2 +- src/api/routes/bots/bot/stats/mod.rs | 2 +- src/api/routes/invitations/invitation/mod.rs | 2 +- src/api/routes/users/user/bots/mod.rs | 47 ++++++++++---------- src/api/routes/users/user/invitations/mod.rs | 18 ++++---- src/api/routes/users/user/mod.rs | 36 ++++++++------- src/utils/logger/codes.rs | 3 -- 9 files changed, 75 insertions(+), 70 deletions(-) diff --git a/src/api/routes/bots/bot/achievements/mod.rs b/src/api/routes/bots/bot/achievements/mod.rs index 1d687da5..2146fdbe 100644 --- a/src/api/routes/bots/bot/achievements/mod.rs +++ b/src/api/routes/bots/bot/achievements/mod.rs @@ -157,7 +157,7 @@ async fn create_achievement( ); return Err(ApiError::Forbidden); } - } else if !ctx.is_user() { + } else { warn!( code = %LogCode::Forbidden, bot_id = %bot_id, @@ -317,7 +317,7 @@ async fn update_achievement( ); return Err(ApiError::Forbidden); } - } else if !ctx.is_user() { + } else { warn!( code = %LogCode::Forbidden, bot_id = %bot_id, @@ -456,7 +456,7 @@ async fn delete_achievement( ); return Err(ApiError::Forbidden); } - } else if !ctx.is_user() { + } else { warn!( code = %LogCode::Forbidden, bot_id = %bot_id, diff --git a/src/api/routes/bots/bot/mod.rs b/src/api/routes/bots/bot/mod.rs index 68071d6d..28cea47f 100644 --- a/src/api/routes/bots/bot/mod.rs +++ b/src/api/routes/bots/bot/mod.rs @@ -258,7 +258,22 @@ async fn patch_bot( let ctx = &auth; - if !(ctx.is_admin() || ctx.is_bot() && ctx.bot_id.as_deref() == Some(bot_id.as_str())) { + if ctx.is_admin() { + info!( + code = %LogCode::AdminAction, + bot_id = %bot_id, + "Admin access granted to update bot", + ); + } else if ctx.is_bot() { + if ctx.token.as_deref() != Some(&bot.token) { + warn!( + code = %LogCode::Forbidden, + bot_id = %bot_id, + "Unauthorized bot update attempt", + ); + return Err(ApiError::Forbidden); + } + } else { warn!( code = %LogCode::Forbidden, bot_id = %bot_id, @@ -277,18 +292,6 @@ async fn patch_bot( return Err(ApiError::BotSuspended); } - if ctx.is_bot() { - let auth_token = ctx.token.as_deref().ok_or(ApiError::InvalidToken)?; - if bot.token() != auth_token { - warn!( - code = %LogCode::InvalidToken, - bot_id = %bot_id, - "Bot token mismatch during update", - ); - return Err(ApiError::InvalidToken); - } - } - let update_data = body.into_inner(); let mut update = BotUpdate::default(); diff --git a/src/api/routes/bots/bot/reports/mod.rs b/src/api/routes/bots/bot/reports/mod.rs index 893b289b..a8011206 100644 --- a/src/api/routes/bots/bot/reports/mod.rs +++ b/src/api/routes/bots/bot/reports/mod.rs @@ -62,7 +62,7 @@ async fn get_subscriptions( ); return Err(ApiError::Forbidden); } - } else if !ctx.is_user() { + } else { warn!( code = %LogCode::Forbidden, bot_id = %bot_id, diff --git a/src/api/routes/bots/bot/stats/mod.rs b/src/api/routes/bots/bot/stats/mod.rs index dabe4f5c..53a5b10c 100644 --- a/src/api/routes/bots/bot/stats/mod.rs +++ b/src/api/routes/bots/bot/stats/mod.rs @@ -225,7 +225,7 @@ async fn post_stats( ); return Err(ApiError::Forbidden); } - } else if !ctx.is_admin() && !ctx.is_bot() { + } else { warn!( code = %LogCode::Forbidden, bot_id = %bot_id, diff --git a/src/api/routes/invitations/invitation/mod.rs b/src/api/routes/invitations/invitation/mod.rs index fc0711c2..36b24af8 100644 --- a/src/api/routes/invitations/invitation/mod.rs +++ b/src/api/routes/invitations/invitation/mod.rs @@ -178,7 +178,7 @@ async fn answer_invitation( user_id = %user_id, "User processing invitation", ); - } else if !ctx.is_user() { + } else { info!( code = %LogCode::Forbidden, invitation_id = %invitation_id, diff --git a/src/api/routes/users/user/bots/mod.rs b/src/api/routes/users/user/bots/mod.rs index 89386de0..b321af9c 100644 --- a/src/api/routes/users/user/bots/mod.rs +++ b/src/api/routes/users/user/bots/mod.rs @@ -1,5 +1,4 @@ use actix_web::web::{Data, Json, Path}; -use anyhow::Result; use apistos::{ api_operation, web::{ServiceConfig, get}, @@ -40,14 +39,16 @@ async fn get_user_bots( user_id = %user_id, "Admin access granted for user bots" ); - } else if ctx.is_user() && ctx.user_id.as_deref() != Some(&user_id) { - warn!( - code = %LogCode::Forbidden, - user_id = %user_id, - "User attempted to access another user's bots" - ); - return Err(ApiError::Forbidden); - } else if !ctx.is_user() { + } else if ctx.is_user() { + if ctx.user_id.as_deref() != Some(&user_id) { + warn!( + code = %LogCode::Forbidden, + user_id = %user_id, + "User attempted to access another user's bots" + ); + return Err(ApiError::Forbidden); + } + } else { warn!( code = %LogCode::Forbidden, user_id = %user_id, @@ -58,21 +59,19 @@ async fn get_user_bots( let user_bots = repos.bots.find_by_user_id(&user_id).await?; - let owned_bots = user_bots - .iter() - .filter(|b| b.owner_id == user_id) - .cloned() - .map(BotResponse::try_from) - .collect::>>()?; - let team_bots = user_bots - .into_iter() - .filter(|b| b.team.contains(&user_id)) - .map(|b| { - let mut res = BotResponse::try_from(b)?; - res.webhooks_config = None; - Ok(res) - }) - .collect::>>()?; + let (owned_bots, team_bots) = user_bots.into_iter().try_fold( + (Vec::new(), Vec::new()), + |(mut owned, mut team), b| -> ApiResult<(Vec, Vec)> { + if b.owner_id == user_id { + owned.push(BotResponse::try_from(b)?); + } else if b.team.contains(&user_id) { + let mut res = BotResponse::try_from(b)?; + res.webhooks_config = None; + team.push(res); + } + Ok((owned, team)) + }, + )?; info!( code = %LogCode::Request, diff --git a/src/api/routes/users/user/invitations/mod.rs b/src/api/routes/users/user/invitations/mod.rs index 55055a82..d28d3252 100644 --- a/src/api/routes/users/user/invitations/mod.rs +++ b/src/api/routes/users/user/invitations/mod.rs @@ -39,14 +39,16 @@ async fn get_user_invitations( user_id = %user_id, "Admin access granted for user invitations" ); - } else if ctx.is_user() && ctx.user_id.as_deref() != Some(&user_id) { - warn!( - code = %LogCode::Forbidden, - user_id = %user_id, - "User attempted to access another user's invitations" - ); - return Err(ApiError::Forbidden); - } else if !ctx.is_user() { + } else if ctx.is_user() { + if ctx.user_id.as_deref() != Some(&user_id) { + warn!( + code = %LogCode::Forbidden, + user_id = %user_id, + "User attempted to access another user's invitations" + ); + return Err(ApiError::Forbidden); + } + } else { warn!( code = %LogCode::Forbidden, user_id = %user_id, diff --git a/src/api/routes/users/user/mod.rs b/src/api/routes/users/user/mod.rs index abe81079..4165abe7 100644 --- a/src/api/routes/users/user/mod.rs +++ b/src/api/routes/users/user/mod.rs @@ -47,14 +47,16 @@ async fn get_user( user_id = %user_id, "Admin access granted for user details" ); - } else if ctx.is_user() && ctx.user_id.as_deref() != Some(&user_id) { - warn!( - code = %LogCode::Forbidden, - user_id = %user_id, - "User attempted to access another user's details" - ); - return Err(ApiError::Forbidden); - } else if !ctx.is_user() { + } else if ctx.is_user() { + if ctx.user_id.as_deref() != Some(&user_id) { + warn!( + code = %LogCode::Forbidden, + user_id = %user_id, + "User attempted to access another user's details" + ); + return Err(ApiError::Forbidden); + } + } else { warn!( code = %LogCode::Forbidden, user_id = %user_id, @@ -162,14 +164,16 @@ async fn delete_user( user_id = %user_id, "Admin access granted for user deletion" ); - } else if ctx.is_user() && ctx.user_id.as_deref() != Some(&user_id) { - warn!( - code = %LogCode::Forbidden, - user_id = %user_id, - "User attempted to delete another user's account" - ); - return Err(ApiError::Forbidden); - } else if !ctx.is_user() { + } else if ctx.is_user() { + if ctx.user_id.as_deref() != Some(&user_id) { + warn!( + code = %LogCode::Forbidden, + user_id = %user_id, + "User attempted to delete another user's account" + ); + return Err(ApiError::Forbidden); + } + } else { warn!( code = %LogCode::Forbidden, user_id = %user_id, diff --git a/src/utils/logger/codes.rs b/src/utils/logger/codes.rs index 41d116ef..37dd28c9 100644 --- a/src/utils/logger/codes.rs +++ b/src/utils/logger/codes.rs @@ -14,8 +14,6 @@ pub enum LogCode { Unauthorized, /// Forbidden access attempts Forbidden, - /// Invalid token or authentication failures - InvalidToken, /// Admin actions AdminAction, /// Bot expiration warnings @@ -50,7 +48,6 @@ impl LogCode { LogCode::Auth => "AUTH", LogCode::Unauthorized => "UNAUTH", LogCode::Forbidden => "FORBID", - LogCode::InvalidToken => "INV_TOKEN", LogCode::AdminAction => "ADMIN", LogCode::BotExpiration => "BOT_EXP", LogCode::User => "USER", From 9c5db7de77583135ef9a32ac7c8ffe049e271547 Mon Sep 17 00:00:00 2001 From: ValDesign Date: Sat, 18 Apr 2026 11:58:03 +0200 Subject: [PATCH 7/9] chore(models): remove unused function --- src/domain/models/bot.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/domain/models/bot.rs b/src/domain/models/bot.rs index e454e8dc..b7831256 100644 --- a/src/domain/models/bot.rs +++ b/src/domain/models/bot.rs @@ -62,10 +62,6 @@ impl Bot { } } - pub fn token(self) -> String { - self.token - } - pub fn is_owner(&self, user_id: &str) -> bool { self.owner_id == user_id } From 33b92b891c47afe1c42ab3791921b39e8c52de74 Mon Sep 17 00:00:00 2001 From: ValDesign Date: Sat, 18 Apr 2026 12:24:14 +0200 Subject: [PATCH 8/9] fix(routes): custom_event current_value should be correct now --- src/api/routes/bots/bot/events/event/mod.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/api/routes/bots/bot/events/event/mod.rs b/src/api/routes/bots/bot/events/event/mod.rs index 77c51e47..b8458907 100644 --- a/src/api/routes/bots/bot/events/event/mod.rs +++ b/src/api/routes/bots/bot/events/event/mod.rs @@ -3,6 +3,7 @@ use apistos::{ api_operation, web::{ServiceConfig, delete, get, patch, resource, scope}, }; +use mongodb::bson::DateTime; use tracing::{error, info, warn}; use crate::{ @@ -102,10 +103,20 @@ async fn get_event( )) })?; + let current_date = DateTime::now(); + let start_of_hour = DateTime::from_millis( + current_date.timestamp_millis() - (current_date.timestamp_millis() % 3600000), + ); + let stats = repos.bot_stats.find_last(&bot_id).await?; - let current_value = stats - .and_then(|s| s.custom_events.get(&event_key).copied()) - .or(event.default_value); + let current_value = match stats { + Some(s) if event.default_value.is_none() || s.date == start_of_hour => s + .custom_events + .get(&event_key) + .copied() + .or(event.default_value), + _ => event.default_value, + }; Ok(Json(CustomEventResponse::new(event, current_value))) } From 38a1d1efba5447c6d8a151e28beef9487a05f87a Mon Sep 17 00:00:00 2001 From: ValDesign Date: Sat, 18 Apr 2026 13:53:50 +0200 Subject: [PATCH 9/9] fix(routes): finally fixed the custom event current_value --- src/api/routes/bots/bot/events/event/mod.rs | 18 +++++++++--------- src/repository/bot_stats.rs | 10 ++++++++-- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/api/routes/bots/bot/events/event/mod.rs b/src/api/routes/bots/bot/events/event/mod.rs index b8458907..e1c4b892 100644 --- a/src/api/routes/bots/bot/events/event/mod.rs +++ b/src/api/routes/bots/bot/events/event/mod.rs @@ -108,15 +108,15 @@ async fn get_event( current_date.timestamp_millis() - (current_date.timestamp_millis() % 3600000), ); - let stats = repos.bot_stats.find_last(&bot_id).await?; - let current_value = match stats { - Some(s) if event.default_value.is_none() || s.date == start_of_hour => s - .custom_events - .get(&event_key) - .copied() - .or(event.default_value), - _ => event.default_value, - }; + let stats = repos + .bot_stats + .find_last_event_occurence(&bot_id, &event_key) + .await?; + + let current_value = stats + .filter(|s| event.default_value.is_none() || s.date == start_of_hour) + .and_then(|s| s.custom_events.get(&event_key).copied()) + .or(event.default_value); Ok(Json(CustomEventResponse::new(event, current_value))) } diff --git a/src/repository/bot_stats.rs b/src/repository/bot_stats.rs index 32b07b0e..f433632b 100644 --- a/src/repository/bot_stats.rs +++ b/src/repository/bot_stats.rs @@ -208,10 +208,16 @@ impl BotStatsRepository { }) } - pub async fn find_last(&self, bot_id: &str) -> Result> { + pub async fn find_last_event_occurence( + &self, + bot_id: &str, + event_key: &str, + ) -> Result> { let options = FindOneOptions::builder().sort(doc! { "date": -1 }).build(); self.collection - .find_one(doc! { "botId": bot_id }) + .find_one( + doc! { "botId": bot_id, format!("customEvents.{event_key}"): { "$exists": true } }, + ) .with_options(options) .await }