From 7bfa9e63b19e2d00a4ba0efd47e08253ce38fe2e Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 28 Dec 2024 08:06:29 +0000 Subject: [PATCH 1/4] get rid of useless stuff, more splitting --- src/config.rs | 5 +- src/panelapi/actions/baseanalytics.rs | 69 + src/panelapi/actions/getuser.rs | 28 + src/panelapi/actions/mod.rs | 6 + src/panelapi/actions/searchentitys.rs | 130 + src/panelapi/actions/updatepartners.rs | 312 +++ src/panelapi/actions/updatestaffmembers.rs | 146 + src/panelapi/actions/updatestaffposition.rs | 601 +++++ src/panelapi/core.rs | 2 - src/panelapi/panel_query.rs | 68 - src/panelapi/server.rs | 2638 ++----------------- src/panelapi/types/cdn.rs | 80 - src/panelapi/types/changelogs.rs | 81 - src/panelapi/types/mod.rs | 2 - src/tasks/__toberewritten/README.md | 3 - src/tasks/__toberewritten/uptime.rs | 116 - 16 files changed, 1480 insertions(+), 2807 deletions(-) create mode 100644 src/panelapi/actions/baseanalytics.rs create mode 100644 src/panelapi/actions/getuser.rs create mode 100644 src/panelapi/actions/searchentitys.rs create mode 100644 src/panelapi/actions/updatepartners.rs create mode 100644 src/panelapi/actions/updatestaffmembers.rs create mode 100644 src/panelapi/actions/updatestaffposition.rs delete mode 100644 src/panelapi/types/cdn.rs delete mode 100644 src/panelapi/types/changelogs.rs delete mode 100644 src/tasks/__toberewritten/README.md delete mode 100644 src/tasks/__toberewritten/uptime.rs diff --git a/src/config.rs b/src/config.rs index 8d0db8f5..a90b2aaf 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,8 +3,6 @@ use once_cell::sync::Lazy; use poise::serenity_prelude::{ChannelId, GuildId, RoleId, UserId}; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, fs::File, io::Write}; -use ts_rs::TS; -use utoipa::ToSchema; pub static CURRENT_ENV: Lazy<&str> = Lazy::new(|| { let current_env = include_bytes!("../current-env"); @@ -124,8 +122,7 @@ pub struct PanelConfig { pub panel_response_scope: String, } -#[derive(Serialize, Deserialize, TS, ToSchema, Clone, Default)] -#[ts(export, export_to = ".generated/CdnScopeData.ts")] +#[derive(Serialize, Deserialize, Clone, Default)] pub struct CdnScopeData { /// Path in local fs (or remote if support is added) pub path: String, diff --git a/src/panelapi/actions/baseanalytics.rs b/src/panelapi/actions/baseanalytics.rs new file mode 100644 index 00000000..5c75256a --- /dev/null +++ b/src/panelapi/actions/baseanalytics.rs @@ -0,0 +1,69 @@ +use crate::panelapi::auth::check_auth; +use crate::panelapi::core::{AppState, Error}; +use crate::panelapi::types::analytics::BaseAnalytics; +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + Json, +}; + +pub async fn base_analytics(state: &AppState, login_token: String) -> Result { + check_auth(&state.pool, &login_token) + .await + .map_err(Error::new)?; + + let bot_counts = sqlx::query!("SELECT type, COUNT(*) FROM bots GROUP BY type") + .fetch_all(&state.pool) + .await + .map_err(Error::new)?; + + let server_counts = sqlx::query!("SELECT type, COUNT(*) FROM servers GROUP BY type") + .fetch_all(&state.pool) + .await + .map_err(Error::new)?; + + let ticket_counts = sqlx::query!("SELECT open, COUNT(*) FROM tickets GROUP BY open") + .fetch_all(&state.pool) + .await + .map_err(Error::new)?; + + let total_users = sqlx::query!("SELECT COUNT(*) FROM users") + .fetch_one(&state.pool) + .await + .map_err(Error::new)?; + + let total_changelogs = sqlx::query!("SELECT COUNT(*) FROM changelogs") + .fetch_one(&state.pool) + .await + .map_err(Error::new)?; + + Ok(( + StatusCode::OK, + Json(BaseAnalytics { + bot_counts: bot_counts + .iter() + .map(|b| (b.r#type.clone(), b.count.unwrap_or_default())) + .collect(), + server_counts: server_counts + .iter() + .map(|s| (s.r#type.clone(), s.count.unwrap_or_default())) + .collect(), + ticket_counts: ticket_counts + .iter() + .map(|t| { + ( + if t.open { + "open".to_string() + } else { + "closed".to_string() + }, + t.count.unwrap_or_default(), + ) + }) + .collect(), + total_users: total_users.count.unwrap_or_default(), + changelogs_count: total_changelogs.count.unwrap_or_default(), + }), + ) + .into_response()) +} diff --git a/src/panelapi/actions/getuser.rs b/src/panelapi/actions/getuser.rs new file mode 100644 index 00000000..68cb3737 --- /dev/null +++ b/src/panelapi/actions/getuser.rs @@ -0,0 +1,28 @@ +use crate::impls::dovewing::{get_platform_user, DovewingSource}; +use crate::panelapi::auth::check_auth; +use crate::panelapi::core::{AppState, Error}; +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + Json, +}; + +pub async fn get_user( + state: &AppState, + login_token: String, + user_id: String, +) -> Result { + check_auth(&state.pool, &login_token) + .await + .map_err(Error::new)?; + + let user = get_platform_user( + &state.pool, + DovewingSource::Discord(state.cache_http.clone()), + &user_id, + ) + .await + .map_err(Error::new)?; + + Ok((StatusCode::OK, Json(user)).into_response()) +} diff --git a/src/panelapi/actions/mod.rs b/src/panelapi/actions/mod.rs index 93117452..1dafef8a 100644 --- a/src/panelapi/actions/mod.rs +++ b/src/panelapi/actions/mod.rs @@ -1,2 +1,8 @@ pub mod authorize; +pub mod baseanalytics; +pub mod getuser; pub mod hello; +pub mod searchentitys; +pub mod updatepartners; +pub mod updatestaffmembers; +pub mod updatestaffposition; diff --git a/src/panelapi/actions/searchentitys.rs b/src/panelapi/actions/searchentitys.rs new file mode 100644 index 00000000..708fd223 --- /dev/null +++ b/src/panelapi/actions/searchentitys.rs @@ -0,0 +1,130 @@ +use crate::impls::dovewing::{get_platform_user, DovewingSource}; +use crate::impls::target_types::TargetType; +use crate::impls::utils::get_entity_managers; +use crate::panelapi::auth::check_auth; +use crate::panelapi::core::{AppState, Error}; +use crate::panelapi::types::entity::{PartialBot, PartialEntity, PartialServer}; +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + Json, +}; + +pub async fn search_entitys( + state: &AppState, + login_token: String, + target_type: TargetType, + query: String, +) -> Result { + check_auth(&state.pool, &login_token) + .await + .map_err(Error::new)?; + + match target_type { + TargetType::Bot => { + let queue = sqlx::query!( + " + SELECT bot_id, client_id, type, approximate_votes, shards, library, invite_clicks, clicks, + servers, last_claimed, claimed_by, approval_note, short, invite FROM bots + INNER JOIN internal_user_cache__discord discord_users ON bots.bot_id = discord_users.id + WHERE bot_id = $1 OR client_id = $1 OR discord_users.username ILIKE $2 ORDER BY bots.created_at + ", + query, + format!("%{}%", query) + ) + .fetch_all(&state.pool) + .await + .map_err(Error::new)?; + + let mut bots = Vec::new(); + + for bot in queue { + let owners = get_entity_managers(TargetType::Bot, &bot.bot_id, &state.pool) + .await + .map_err(Error::new)?; + + let user = get_platform_user( + &state.pool, + DovewingSource::Discord(state.cache_http.clone()), + &bot.bot_id, + ) + .await + .map_err(Error::new)?; + + bots.push(PartialEntity::Bot(PartialBot { + bot_id: bot.bot_id, + client_id: bot.client_id, + user, + r#type: bot.r#type, + votes: bot.approximate_votes, + shards: bot.shards, + library: bot.library, + invite_clicks: bot.invite_clicks, + clicks: bot.clicks, + servers: bot.servers, + claimed_by: bot.claimed_by, + last_claimed: bot.last_claimed, + approval_note: bot.approval_note, + short: bot.short, + mentionable: owners.mentionables(), + invite: bot.invite, + })); + } + + Ok((StatusCode::OK, Json(bots)).into_response()) + } + TargetType::Server => { + let queue = sqlx::query!( + " + SELECT server_id, name, total_members, online_members, short, type, approximate_votes, invite_clicks, + clicks, nsfw, tags, premium, claimed_by, last_claimed FROM servers + WHERE server_id = $1 OR name ILIKE $2 ORDER BY created_at + ", + query, + format!("%{}%", query) + ) + .fetch_all(&state.pool) + .await + .map_err(Error::new)?; + + let mut servers = Vec::new(); + + for server in queue { + let owners = + get_entity_managers(TargetType::Server, &server.server_id, &state.pool) + .await + .map_err(Error::new)?; + + servers.push(PartialEntity::Server(PartialServer { + server_id: server.server_id.clone(), + name: server.name, + avatar: format!( + "{}/servers/avatars/{}.webp", + crate::config::CONFIG.cdn_url, + server.server_id + ), + total_members: server.total_members, + online_members: server.online_members, + short: server.short, + r#type: server.r#type, + votes: server.approximate_votes, + invite_clicks: server.invite_clicks, + clicks: server.clicks, + nsfw: server.nsfw, + tags: server.tags, + premium: server.premium, + claimed_by: server.claimed_by, + last_claimed: server.last_claimed, + mentionable: owners.mentionables(), + })); + } + + Ok((StatusCode::OK, Json(servers)).into_response()) + } + _ => Ok(( + StatusCode::NOT_IMPLEMENTED, + "Searching this target type is not implemented".to_string(), + ) + .into_response()), + } +} diff --git a/src/panelapi/actions/updatepartners.rs b/src/panelapi/actions/updatepartners.rs new file mode 100644 index 00000000..51cf15c9 --- /dev/null +++ b/src/panelapi/actions/updatepartners.rs @@ -0,0 +1,312 @@ +use crate::impls::utils::get_user_perms; +use crate::panelapi::auth::check_auth; +use crate::panelapi::core::{AppState, Error}; +use crate::panelapi::types::partners::{ + CreatePartner, Partner, PartnerAction, PartnerType, Partners, +}; +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + Json, +}; +use kittycat::perms; +use sqlx::PgPool; + +pub async fn update_partners( + state: &AppState, + login_token: String, + action: PartnerAction, +) -> Result { + let auth_data = check_auth(&state.pool, &login_token) + .await + .map_err(Error::new)?; + + let user_perms = get_user_perms(&state.pool, &auth_data.user_id) + .await + .map_err(Error::new)? + .resolve(); + + async fn parse_partner(pool: &PgPool, partner: &CreatePartner) -> Result<(), crate::Error> { + // Check if partner type exists + let partner_type_exists = + sqlx::query!("SELECT id FROM partner_types WHERE id = $1", partner.r#type) + .fetch_optional(pool) + .await? + .is_some(); + + if !partner_type_exists { + return Err("Partner type does not exist".into()); + } + + // Ensure that image has been uploaded to CDN + // Get cdn path from cdn_scope hashmap + let cdn_scopes = crate::config::CONFIG.panel.cdn_scopes.get(); + + let Some(cdn_path) = cdn_scopes.get(&crate::config::CONFIG.panel.main_scope) else { + return Err("Main scope not found".into()); + }; + + let path = format!("{}/avatars/partners/{}.webp", cdn_path.path, partner.id); + + match std::fs::metadata(&path) { + Ok(m) => { + if !m.is_file() { + return Err("Image does not exist".into()); + } + + if m.len() > 100_000_000 { + return Err("Image is too large".into()); + } + + if m.len() == 0 { + return Err("Image is empty".into()); + } + } + Err(e) => { + return Err( + ("Fetching image metadata failed: ".to_string() + &e.to_string()).into(), + ); + } + }; + + if partner.links.is_empty() { + return Err("Links cannot be empty".into()); + } + + for link in &partner.links { + if link.name.is_empty() { + return Err("Link name cannot be empty".into()); + } + + if link.value.is_empty() { + return Err("Link URL cannot be empty".into()); + } + + if !link.value.starts_with("https://") { + return Err("Link URL must start with https://".into()); + } + } + + // Check user id + let user_exists = sqlx::query!( + "SELECT user_id FROM users WHERE user_id = $1", + partner.user_id + ) + .fetch_optional(pool) + .await? + .is_some(); + + if !user_exists { + return Err("User does not exist".into()); + } + + Ok(()) + } + + match action { + PartnerAction::List => { + let prec = sqlx::query!( + "SELECT id, name, short, links, type, created_at, user_id, bot_id FROM partners" + ) + .fetch_all(&state.pool) + .await + .map_err(Error::new)?; + + let mut partners = Vec::new(); + + for partner in prec { + partners.push(Partner { + id: partner.id, + name: partner.name, + short: partner.short, + links: serde_json::from_value(partner.links).map_err(Error::new)?, + r#type: partner.r#type, + created_at: partner.created_at, + user_id: partner.user_id, + bot_id: partner.bot_id, + }) + } + + let ptrec = sqlx::query!("SELECT id, name, short, icon, created_at FROM partner_types") + .fetch_all(&state.pool) + .await + .map_err(Error::new)?; + + let mut partner_types = Vec::new(); + + for partner_type in ptrec { + partner_types.push(PartnerType { + id: partner_type.id, + name: partner_type.name, + short: partner_type.short, + icon: partner_type.icon, + created_at: partner_type.created_at, + }) + } + + Ok(( + StatusCode::OK, + Json(Partners { + partners, + partner_types, + }), + ) + .into_response()) + } + PartnerAction::Create { partner } => { + if !perms::has_perm(&user_perms, &"partners.create".into()) { + return Ok(( + StatusCode::FORBIDDEN, + "You do not have permission to create partners [partners.create]".to_string(), + ) + .into_response()); + } + + // Check if partner already exists + let partner_exists = sqlx::query!("SELECT id FROM partners WHERE id = $1", partner.id) + .fetch_optional(&state.pool) + .await + .map_err(Error::new)? + .is_some(); + + if partner_exists { + return Ok(( + StatusCode::BAD_REQUEST, + "Partner already exists".to_string(), + ) + .into_response()); + } + + if let Err(e) = parse_partner(&state.pool, &partner).await { + return Ok((StatusCode::BAD_REQUEST, e.to_string()).into_response()); + } + + // Insert partner + sqlx::query!( + "INSERT INTO partners (id, name, short, links, type, user_id, bot_id) VALUES ($1, $2, $3, $4, $5, $6, $7)", + partner.id, + partner.name, + partner.short, + serde_json::to_value(partner.links).map_err(Error::new)?, + partner.r#type, + partner.user_id, + partner.bot_id + ) + .execute(&state.pool) + .await + .map_err(Error::new)?; + + Ok((StatusCode::NO_CONTENT, "").into_response()) + } + PartnerAction::Update { partner } => { + if !perms::has_perm(&user_perms, &"partners.update".into()) { + return Ok(( + StatusCode::FORBIDDEN, + "You do not have permission to update partners [partners.update]".to_string(), + ) + .into_response()); + } + + // Check if partner already exists + let partner_exists = sqlx::query!("SELECT id FROM partners WHERE id = $1", partner.id) + .fetch_optional(&state.pool) + .await + .map_err(Error::new)? + .is_some(); + + if !partner_exists { + return Ok(( + StatusCode::BAD_REQUEST, + "Partner does not already exist".to_string(), + ) + .into_response()); + } + + if let Err(e) = parse_partner(&state.pool, &partner).await { + return Ok((StatusCode::BAD_REQUEST, e.to_string()).into_response()); + } + + // Update partner + sqlx::query!( + "UPDATE partners SET name = $2, short = $3, links = $4, type = $5, user_id = $6, bot_id = $7 WHERE id = $1", + partner.id, + partner.name, + partner.short, + serde_json::to_value(partner.links).map_err(Error::new)?, + partner.r#type, + partner.user_id, + partner.bot_id + ) + .execute(&state.pool) + .await + .map_err(Error::new)?; + + Ok((StatusCode::NO_CONTENT, "").into_response()) + } + PartnerAction::Delete { id } => { + if !perms::has_perm(&user_perms, &"partners.delete".into()) { + return Ok(( + StatusCode::FORBIDDEN, + "You do not have permission to delete partners [partners.delete]".to_string(), + ) + .into_response()); + } + + // Check if partner exists + let partner_exists = sqlx::query!("SELECT id FROM partners WHERE id = $1", id) + .fetch_optional(&state.pool) + .await + .map_err(Error::new)? + .is_some(); + + if !partner_exists { + return Ok(( + StatusCode::BAD_REQUEST, + "Partner does not exist".to_string(), + ) + .into_response()); + } + + // Ensure that image has been uploaded to CDN + // Get cdn path from cdn_scope hashmap + let cdn_scopes = crate::config::CONFIG.panel.cdn_scopes.get(); + + let Some(cdn_path) = cdn_scopes.get(&crate::config::CONFIG.panel.main_scope) else { + return Ok( + (StatusCode::BAD_REQUEST, "Main scope not found".to_string()).into_response(), + ); + }; + + let path = format!("{}/partners/{}.webp", cdn_path.path, id); + + match std::fs::metadata(&path) { + Ok(m) => { + if m.is_symlink() || m.is_file() { + // Delete the symlink + std::fs::remove_file(path).map_err(Error::new)?; + } else if m.is_dir() { + // Delete the directory + std::fs::remove_dir_all(path).map_err(Error::new)?; + } + } + Err(e) => { + if e.kind() != std::io::ErrorKind::NotFound { + return Ok(( + StatusCode::BAD_REQUEST, + "Fetching asset metadata failed due to unknown error: ".to_string() + + &e.to_string(), + ) + .into_response()); + } + } + }; + + sqlx::query!("DELETE FROM partners WHERE id = $1", id) + .execute(&state.pool) + .await + .map_err(Error::new)?; + + Ok((StatusCode::NO_CONTENT, "").into_response()) + } + } +} diff --git a/src/panelapi/actions/updatestaffmembers.rs b/src/panelapi/actions/updatestaffmembers.rs new file mode 100644 index 00000000..3af4c16a --- /dev/null +++ b/src/panelapi/actions/updatestaffmembers.rs @@ -0,0 +1,146 @@ +use crate::panelapi::auth::{check_auth, get_staff_member}; +use crate::panelapi::core::{AppState, Error}; +use crate::panelapi::types::staff_members::StaffMemberAction; +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + Json, +}; +use kittycat::perms::{self, Permission}; + +pub async fn update_staff_members( + state: &AppState, + login_token: String, + action: StaffMemberAction, +) -> Result { + let auth_data = check_auth(&state.pool, &login_token) + .await + .map_err(Error::new)?; + + match action { + StaffMemberAction::ListMembers => { + let ids = sqlx::query!("SELECT user_id FROM staff_members") + .fetch_all(&state.pool) + .await + .map_err(|e| format!("Error while getting staff members {}", e)) + .map_err(Error::new)?; + + let mut members = Vec::new(); + + for id in ids { + let member = get_staff_member(&state.pool, &state.cache_http, &id.user_id) + .await + .map_err(Error::new)?; + + members.push(member); + } + + Ok((StatusCode::OK, Json(members)).into_response()) + } + StaffMemberAction::EditMember { + user_id, + perm_overrides, + no_autosync, + unaccounted, + } => { + // Get permissions + let sm = get_staff_member(&state.pool, &state.cache_http, &auth_data.user_id) + .await + .map_err(Error::new)?; + + // Get permissions of target + let sm_target = get_staff_member(&state.pool, &state.cache_http, &user_id) + .await + .map_err(Error::new)?; + + if !perms::has_perm(&sm.resolved_perms, &"staff_members.edit".into()) { + return Ok(( + StatusCode::FORBIDDEN, + "You do not have permission to edit staff members [staff_members.edit]" + .to_string(), + ) + .into_response()); + } + + // Get the lowest index permission of the member + let mut sm_lowest_index = i32::MAX; + + for perm in &sm.positions { + if perm.index < sm_lowest_index { + sm_lowest_index = perm.index; + } + } + + // Get the lowest index permission of the target + let mut sm_target_lowest_index = i32::MAX; + + for perm in &sm_target.positions { + if perm.index < sm_target_lowest_index { + sm_target_lowest_index = perm.index; + } + } + + // If the target has a lower index than the member, then error + if sm_target_lowest_index < sm_lowest_index { + return Ok(( + StatusCode::FORBIDDEN, + "Target has a lower index than the member".to_string(), + ) + .into_response()); + } + + let perm_overrides = perm_overrides + .iter() + .map(|x| Permission::from_string(x)) + .collect::>(); + + // Check perms with resolved perms following addition of overrides + let new_resolved_perms = perms::StaffPermissions { + perm_overrides: perm_overrides.clone(), + ..sm_target.staff_permission + } + .resolve(); + + if let Err(e) = perms::check_patch_changes( + &sm.resolved_perms, + &sm_target.resolved_perms, + &new_resolved_perms, + ) { + return Ok(( + StatusCode::FORBIDDEN, + format!( + "You do not have permission to edit the following perms: {}", + e + ), + ) + .into_response()); + } + + // Then update + let mut tx = state.pool.begin().await.map_err(Error::new)?; + + // Lock the member for update + sqlx::query!("SELECT perm_overrides, no_autosync, unaccounted FROM staff_members WHERE user_id = $1 FOR UPDATE", user_id) + .fetch_one(&mut *tx) + .await + .map_err(|e| format!("Error while getting member {}", e)) + .map_err(Error::new)?; + + // Update the member + sqlx::query!("UPDATE staff_members SET perm_overrides = $1, no_autosync = $2, unaccounted = $3 WHERE user_id = $4", + &perm_overrides.iter().map(|x| x.to_string()).collect::>(), + no_autosync, + unaccounted, + user_id + ) + .execute(&mut *tx) + .await + .map_err(|e| format!("Error while updating member {}", e)) + .map_err(Error::new)?; + + tx.commit().await.map_err(Error::new)?; + + Ok((StatusCode::NO_CONTENT, "").into_response()) + } + } +} diff --git a/src/panelapi/actions/updatestaffposition.rs b/src/panelapi/actions/updatestaffposition.rs new file mode 100644 index 00000000..15f9d7a7 --- /dev/null +++ b/src/panelapi/actions/updatestaffposition.rs @@ -0,0 +1,601 @@ +use std::str::FromStr; + +use crate::panelapi::auth::{check_auth, get_staff_member}; +use crate::panelapi::core::{AppState, Error}; +use crate::panelapi::types::staff_positions::{ + CorrespondingServer, StaffPosition, StaffPositionAction, +}; +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + Json, +}; +use kittycat::perms::{self, Permission}; +use serenity::all::RoleId; +use strum::VariantNames; + +pub async fn update_staff_position( + state: &AppState, + login_token: String, + action: StaffPositionAction, +) -> Result { + let auth_data = check_auth(&state.pool, &login_token) + .await + .map_err(Error::new)?; + + match action { + StaffPositionAction::ListPositions => { + let pos = sqlx::query!("SELECT id, name, role_id, perms, corresponding_roles, icon, index, created_at FROM staff_positions ORDER BY index ASC") + .fetch_all(&state.pool) + .await + .map_err(|e| format!("Error while getting staff positions {}", e)) + .map_err(Error::new)?; + + let mut positions = Vec::new(); + + for position_data in pos { + positions.push(StaffPosition { + id: position_data.id.hyphenated().to_string(), + name: position_data.name, + role_id: position_data.role_id, + perms: position_data.perms, + corresponding_roles: serde_json::from_value(position_data.corresponding_roles) + .map_err(Error::new)?, + icon: position_data.icon, + index: position_data.index, + created_at: position_data.created_at, + }); + } + + Ok((StatusCode::OK, Json(positions)).into_response()) + } + StaffPositionAction::SwapIndex { a, b } => { + // Get permissions + let sm = get_staff_member(&state.pool, &state.cache_http, &auth_data.user_id) + .await + .map_err(Error::new)?; + + if !perms::has_perm(&sm.resolved_perms, &"staff_positions.swap_index".into()) { + return Ok(( + StatusCode::FORBIDDEN, + "You do not have permission to swap indexes of staff positions [staff_positions.swap_index]".to_string(), + ) + .into_response()); + } + + // Get the lowest index permission of the member + let mut sm_lowest_index = i32::MAX; + + for perm in &sm.positions { + if perm.index < sm_lowest_index { + sm_lowest_index = perm.index; + } + } + + let mut tx = state.pool.begin().await.map_err(Error::new)?; + + let index_a = sqlx::query!("SELECT index FROM staff_positions WHERE id::text = $1", a) + .fetch_one(&mut *tx) + .await + .map_err(|e| format!("Error while getting lower position {}", e)) + .map_err(Error::new)? + .index; + + // Get the higher staff positions index + let index_b = sqlx::query!("SELECT index FROM staff_positions WHERE id::text = $1", b) + .fetch_one(&mut *tx) + .await + .map_err(|e| format!("Error while getting higher position {}", e)) + .map_err(Error::new)? + .index; + + if index_a == index_b { + return Ok(( + StatusCode::BAD_REQUEST, + "Positions have the same index".to_string(), + ) + .into_response()); + } + + // If either a or b is lower than the lowest index of the member, then error + if index_a <= sm_lowest_index || index_b <= sm_lowest_index { + return Ok(( + StatusCode::FORBIDDEN, + "Either 'a' or 'b' is lower than the lowest index of the member".to_string(), + ) + .into_response()); + } + + // Swap the indexes + sqlx::query!( + "UPDATE staff_positions SET index = $1 WHERE id::text = $2", + index_b, + a + ) + .execute(&mut *tx) + .await + .map_err(|e| format!("Error while updating lower position {}", e)) + .map_err(Error::new)?; + + sqlx::query!( + "UPDATE staff_positions SET index = $1 WHERE id::text = $2", + index_a, + b + ) + .execute(&mut *tx) + .await + .map_err(|e| format!("Error while updating higher position {}", e)) + .map_err(Error::new)?; + + tx.commit().await.map_err(Error::new)?; + + Ok((StatusCode::NO_CONTENT, "").into_response()) + } + StaffPositionAction::SetIndex { id, index } => { + let uuid = sqlx::types::uuid::Uuid::parse_str(&id).map_err(Error::new)?; + + // Get permissions + let sm = get_staff_member(&state.pool, &state.cache_http, &auth_data.user_id) + .await + .map_err(Error::new)?; + + if !perms::has_perm(&sm.resolved_perms, &"staff_positions.set_index".into()) { + return Ok(( + StatusCode::FORBIDDEN, + "You do not have permission to set the indexes of staff positions [staff_positions.set_index]".to_string(), + ) + .into_response()); + } + + if index < 0 { + return Ok(( + StatusCode::BAD_REQUEST, + "Index cannot be lower than 0".to_string(), + ) + .into_response()); + } + + // Get the lowest index permission of the member + let mut sm_lowest_index = i32::MAX; + + for perm in &sm.positions { + if perm.index < sm_lowest_index { + sm_lowest_index = perm.index; + } + } + + if index <= sm_lowest_index { + return Ok(( + StatusCode::FORBIDDEN, + "Index to set is lower than or equal to the lowest index of the staff member" + .to_string(), + ) + .into_response()); + } + + let mut tx = state.pool.begin().await.map_err(Error::new)?; + + let curr_index = sqlx::query!("SELECT index FROM staff_positions WHERE id = $1", uuid) + .fetch_one(&mut *tx) + .await + .map_err(|e| format!("Error while getting position {}", e)) + .map_err(Error::new)? + .index; + + // If the current index is lower than the lowest index of the member, then error + if curr_index <= sm_lowest_index { + return Ok(( + StatusCode::FORBIDDEN, + "Current index of position is lower than or equal to the lowest index of the staff member".to_string(), + ) + .into_response()); + } + + // Shift indexes one lower + sqlx::query!( + "UPDATE staff_positions SET index = index + 1 WHERE index >= $1", + index + ) + .execute(&mut *tx) + .await + .map_err(|e| format!("Error while shifting indexes {}", e)) + .map_err(Error::new)?; + + // Set the index + sqlx::query!( + "UPDATE staff_positions SET index = $1 WHERE id = $2", + index, + uuid + ) + .execute(&mut *tx) + .await + .map_err(|e| format!("Error while updating position {}", e)) + .map_err(Error::new)?; + + tx.commit().await.map_err(Error::new)?; + + Ok((StatusCode::NO_CONTENT, "").into_response()) + } + StaffPositionAction::CreatePosition { + name, + role_id, + perms, + index, + corresponding_roles, + icon, + } => { + // Get permissions + let sm = get_staff_member(&state.pool, &state.cache_http, &auth_data.user_id) + .await + .map_err(Error::new)?; + + if !perms::has_perm(&sm.resolved_perms, &"staff_positions.create".into()) { + return Ok(( + StatusCode::FORBIDDEN, + "You do not have permission to create staff positions [staff_positions.create]" + .to_string(), + ) + .into_response()); + } + + if index < 0 { + return Ok(( + StatusCode::BAD_REQUEST, + "Index cannot be lower than 0".to_string(), + ) + .into_response()); + } + + // Get the lowest index permission of the member + let mut sm_lowest_index = i32::MAX; + + for perm in &sm.positions { + if perm.index < sm_lowest_index { + sm_lowest_index = perm.index; + } + } + + if index <= sm_lowest_index { + return Ok(( + StatusCode::FORBIDDEN, + "Index is lower than or equal to the lowest index of the staff member" + .to_string(), + ) + .into_response()); + } + + // Shift indexes one lower + let mut tx = state.pool.begin().await.map_err(Error::new)?; + sqlx::query!( + "UPDATE staff_positions SET index = index + 1 WHERE index >= $1", + index + ) + .execute(&mut *tx) + .await + .map_err(|e| format!("Error while shifting indexes {}", e)) + .map_err(Error::new)?; + + // Ensure role id exists on the staff server + let role_id_snow = role_id.parse::().map_err(Error::new)?; + let role_exists = { + let guild = state + .cache_http + .cache + .guild(crate::config::CONFIG.servers.staff); + + if let Some(guild) = guild { + guild.roles.get(&role_id_snow).is_some() + } else { + false + } + }; + + if !role_exists { + return Ok(( + StatusCode::BAD_REQUEST, + "Role does not exist on the staff server".to_string(), + ) + .into_response()); + } + + // Ensure all corresponding_roles exist on the named server if + for role in corresponding_roles.iter() { + let Ok(corr_server) = CorrespondingServer::from_str(&role.name) else { + return Ok(( + StatusCode::BAD_REQUEST, + format!( + "Server {} is not a supported corresponding role. Supported: {:#?}", + role.name, + CorrespondingServer::VARIANTS + ), + ) + .into_response()); + }; + let role_id_snow = role.value.parse::().map_err(Error::new)?; + + let role_exists = { + let guild = state.cache_http.cache.guild(corr_server.get_id()); + + if let Some(guild) = guild { + guild.roles.get(&role_id_snow).is_some() + } else { + false + } + }; + + if !role_exists { + return Ok(( + StatusCode::BAD_REQUEST, + format!( + "Role {} does not exist on the server {}", + role_id_snow, + corr_server.get_id() + ), + ) + .into_response()); + } + } + + // Create the position + sqlx::query!( + "INSERT INTO staff_positions (name, perms, corresponding_roles, icon, role_id, index) VALUES ($1, $2, $3, $4, $5, $6)", + name, + &perms, + serde_json::to_value(corresponding_roles).map_err(Error::new)?, + icon, + role_id, + index, + ) + .execute(&mut *tx) + .await + .map_err(|e| format!("Error while updating position {}", e)) + .map_err(Error::new)?; + + tx.commit().await.map_err(Error::new)?; + + Ok((StatusCode::NO_CONTENT, "").into_response()) + } + StaffPositionAction::EditPosition { + id, + name, + role_id, + perms, + corresponding_roles, + icon, + } => { + let uuid = sqlx::types::uuid::Uuid::parse_str(&id).map_err(Error::new)?; + + // Get permissions + let sm = get_staff_member(&state.pool, &state.cache_http, &auth_data.user_id) + .await + .map_err(Error::new)?; + + if !perms::has_perm(&sm.resolved_perms, &"staff_positions.edit".into()) { + return Ok(( + StatusCode::FORBIDDEN, + "You do not have permission to edit staff positions [staff_positions.edit]" + .to_string(), + ) + .into_response()); + } + + // Get the lowest index permission of the member + let mut sm_lowest_index = i32::MAX; + + for perm in &sm.positions { + if perm.index < sm_lowest_index { + sm_lowest_index = perm.index; + } + } + + let mut tx = state.pool.begin().await.map_err(Error::new)?; + + // Get the index and current permissions of the position + let index = sqlx::query!( + "SELECT perms, index, role_id FROM staff_positions WHERE id = $1 FOR UPDATE", + uuid + ) + .fetch_one(&mut *tx) + .await + .map_err(|e| format!("Error while getting position {}", e)) + .map_err(Error::new)?; + + // If the index is lower than the lowest index of the member, then error + if index.index <= sm_lowest_index { + return Ok(( + StatusCode::FORBIDDEN, + "Index is lower than the lowest index of the member".to_string(), + ) + .into_response()); + } + + // Check perms + if let Err(e) = perms::check_patch_changes( + &sm.resolved_perms, + &index + .perms + .iter() + .map(|x| Permission::from_string(x)) + .collect::>(), + &perms + .iter() + .map(|x| Permission::from_string(x)) + .collect::>(), + ) { + return Ok(( + StatusCode::FORBIDDEN, + format!( + "You do not have permission to edit the following perms: {}", + e + ), + ) + .into_response()); + } + + // Ensure role id exists on the staff server + let role_id_snow = role_id.parse::().map_err(Error::new)?; + let role_exists = { + let guild = state + .cache_http + .cache + .guild(crate::config::CONFIG.servers.staff); + + if let Some(guild) = guild { + guild.roles.get(&role_id_snow).is_some() + } else { + false + } + }; + + if !role_exists { + return Ok(( + StatusCode::BAD_REQUEST, + "Role does not exist on the staff server".to_string(), + ) + .into_response()); + } + + // Ensure all corresponding_roles exist on the named server if + for role in corresponding_roles.iter() { + let Ok(corr_server) = CorrespondingServer::from_str(&role.name) else { + return Ok(( + StatusCode::BAD_REQUEST, + format!( + "Server {} is not a supported corresponding role. Supported: {:#?}", + role.name, + CorrespondingServer::VARIANTS + ), + ) + .into_response()); + }; + let role_id_snow = role.value.parse::().map_err(Error::new)?; + + let role_exists = { + let guild = state.cache_http.cache.guild(corr_server.get_id()); + + if let Some(guild) = guild { + guild.roles.get(&role_id_snow).is_some() + } else { + false + } + }; + + if !role_exists { + return Ok(( + StatusCode::BAD_REQUEST, + format!( + "Role {} does not exist on the server {}", + role_id_snow, + corr_server.get_id() + ), + ) + .into_response()); + } + } + + // Update the position + sqlx::query!( + "UPDATE staff_positions SET name = $1, perms = $2, corresponding_roles = $3, role_id = $4, icon = $5 WHERE id = $6", + name, + &perms, + serde_json::to_value(corresponding_roles).map_err(Error::new)?, + role_id, + icon, + uuid + ) + .execute(&mut *tx) + .await + .map_err(|e| format!("Error while updating position {}", e)) + .map_err(Error::new)?; + + tx.commit().await.map_err(Error::new)?; + + Ok((StatusCode::NO_CONTENT, "").into_response()) + } + StaffPositionAction::DeletePosition { id } => { + let uuid = sqlx::types::uuid::Uuid::parse_str(&id).map_err(Error::new)?; + + // Get permissions + let sm = get_staff_member(&state.pool, &state.cache_http, &auth_data.user_id) + .await + .map_err(Error::new)?; + + if !perms::has_perm(&sm.resolved_perms, &"staff_positions.delete".into()) { + return Ok(( + StatusCode::FORBIDDEN, + "You do not have permission to delete staff positions [staff_positions.delete]" + .to_string(), + ) + .into_response()); + } + + // Get the lowest index permission of the member + let mut sm_lowest_index = i32::MAX; + + for perm in &sm.positions { + if perm.index < sm_lowest_index { + sm_lowest_index = perm.index; + } + } + + let mut tx = state.pool.begin().await.map_err(Error::new)?; + + // Get the index and current permissions of the position + let index = sqlx::query!( + "SELECT perms, index, role_id FROM staff_positions WHERE id = $1 FOR UPDATE", + uuid + ) + .fetch_one(&mut *tx) + .await + .map_err(|e| format!("Error while getting position {}", e)) + .map_err(Error::new)?; + + // If the index is lower than the lowest index of the member, then error + if index.index <= sm_lowest_index { + return Ok(( + StatusCode::FORBIDDEN, + "Index is lower than the lowest index of the member".to_string(), + ) + .into_response()); + } + + // Check perms + if let Err(e) = perms::check_patch_changes( + &sm.resolved_perms, + &index + .perms + .iter() + .map(|x| Permission::from_string(x)) + .collect::>(), + &Vec::new(), + ) { + return Ok(( + StatusCode::FORBIDDEN, + format!("You do not have permission to edit the following perms [neeed to delete position]: {}", e), + ) + .into_response()); + } + + // Delete the position + sqlx::query!("DELETE FROM staff_positions WHERE id = $1", uuid) + .execute(&mut *tx) + .await + .map_err(|e| format!("Error while deleting position {}", e)) + .map_err(Error::new)?; + + // Shift back indexes one lower + sqlx::query!( + "UPDATE staff_positions SET index = index - 1 WHERE index > $1", + index.index + ) + .execute(&mut *tx) + .await + .map_err(|e| format!("Error while shifting indexes {}", e)) + .map_err(Error::new)?; + + tx.commit().await.map_err(Error::new)?; + + Ok((StatusCode::NO_CONTENT, "").into_response()) + } + } +} diff --git a/src/panelapi/core.rs b/src/panelapi/core.rs index e5488207..68e6f968 100644 --- a/src/panelapi/core.rs +++ b/src/panelapi/core.rs @@ -2,7 +2,6 @@ use axum::{ http::StatusCode, response::{IntoResponse, Response}, }; -use moka::future::Cache; use std::fmt::Display; pub struct Error { @@ -28,5 +27,4 @@ impl IntoResponse for Error { pub struct AppState { pub cache_http: botox::cache::CacheHttpImpl, pub pool: sqlx::PgPool, - pub cdn_file_chunks_cache: Cache>, } diff --git a/src/panelapi/panel_query.rs b/src/panelapi/panel_query.rs index 7ce697ca..36d8fd39 100644 --- a/src/panelapi/panel_query.rs +++ b/src/panelapi/panel_query.rs @@ -5,8 +5,6 @@ use crate::panelapi::types::{ auth::AuthorizeAction, blog::BlogAction, bot_whitelist::BotWhitelistAction, - cdn::CdnAssetAction, - changelogs::ChangelogAction, partners::PartnerAction, shop_items::{ShopCouponAction, ShopItemAction, ShopItemBenefitAction}, staff_disciplinary::StaffDisciplinaryTypeAction, @@ -92,54 +90,6 @@ pub enum PanelQuery { /// Query query: String, }, - /// Uploads a chunk of data returning a chunk ID - /// - /// Chunks expire after 10 minutes and are stored in memory - /// - /// After uploading all chunks for a file, use `AddFile` to create the file - /// - /// Needs `cdn.upload_chunk` permission - UploadCdnFileChunk { - /// Login token - login_token: String, - /// Array of bytes of the chunk contents - chunk: Vec, - }, - /// Lists all available CDN scopes - /// - /// Needs `cdn.list_scopes` permission - ListCdnScopes { - /// Login token - login_token: String, - }, - /// Returns the main CDN scope for Infinity List - /// - /// This is public to all staff members - GetMainCdnScope { - /// Login token - login_token: String, - }, - /// Updates/handles an asset on the CDN - /// - /// Needs `cdn.update_asset` permission. Not yet granular/action specific - UpdateCdnAsset { - /// Login token - login_token: String, - /// CDN scope - /// - /// This describes a location where the CDN may be stored on disk and should be a full path to it - /// - /// Currently the panel uses the following scopes: - /// - /// `ibl@main` - cdn_scope: String, - /// Asset name - name: String, - /// Path - path: String, - /// Action to take - action: CdnAssetAction, - }, /// Updates/handles partners UpdatePartners { /// Login token @@ -147,13 +97,6 @@ pub enum PanelQuery { /// Action action: PartnerAction, }, - /// Updates/handles the changelog of the list - UpdateChangelog { - /// Login token - login_token: String, - /// Action - action: ChangelogAction, - }, /// Updates/handles the blog of the list UpdateBlog { /// Login token @@ -217,15 +160,4 @@ pub enum PanelQuery { /// Action action: BotWhitelistAction, }, - /// Create a request to a/an Popplio staff endpoint - PopplioStaff { - /// Login token - login_token: String, - /// Path - path: String, - /// Method - method: String, - /// Body - body: String, - }, } diff --git a/src/panelapi/server.rs b/src/panelapi/server.rs index 4503c6a1..f1563e77 100644 --- a/src/panelapi/server.rs +++ b/src/panelapi/server.rs @@ -1,21 +1,16 @@ -use std::os::unix::prelude::PermissionsExt; use std::str::FromStr; use std::sync::Arc; -use std::time::Duration; use crate::impls::link::Link; use crate::impls::{target_types::TargetType, utils::get_user_perms}; use crate::panelapi::panel_query::PanelQuery; use crate::panelapi::types::staff_disciplinary::StaffDisciplinaryType; use crate::panelapi::types::{ - analytics::BaseAnalytics, auth::AuthorizeAction, blog::{BlogAction, BlogPost}, bot_whitelist::{BotWhitelist, BotWhitelistAction}, - cdn::{CdnAssetAction, CdnAssetItem}, - changelogs::{ChangelogAction, ChangelogEntry}, - entity::{PartialBot, PartialEntity, PartialServer}, - partners::{CreatePartner, Partner, PartnerAction, PartnerType, Partners}, + entity::{PartialBot, PartialEntity}, + partners::{CreatePartner, PartnerAction}, rpc::RPCWebAction, rpclogs::RPCLogEntry, shop_items::{ @@ -23,7 +18,6 @@ use crate::panelapi::types::{ ShopItemBenefitAction, }, staff_disciplinary::StaffDisciplinaryTypeAction, - staff_positions::StaffPosition, vote_credit_tiers::{VoteCreditTier, VoteCreditTierAction}, webcore::InstanceConfig, }; @@ -37,17 +31,13 @@ use axum::response::IntoResponse; use axum::routing::{get, post}; use axum::{extract::State, http::StatusCode, Router}; use log::info; -use moka::future::Cache; -use serenity::all::RoleId; use sqlx::PgPool; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tower_http::cors::{Any, CorsLayer}; use super::core::{AppState, Error}; use super::types::staff_members::StaffMemberAction; -use super::types::staff_positions::{CorrespondingServer, StaffPositionAction}; +use super::types::staff_positions::StaffPositionAction; use crate::impls::dovewing::DovewingSource; -use sha2::{Digest, Sha512}; use strum::VariantNames; use num_traits::ToPrimitive; @@ -62,11 +52,9 @@ pub async fn init_panelapi(pool: PgPool, cache_http: botox::cache::CacheHttpImpl InstanceConfig, RPCMethod, TargetType, - CdnAssetAction, PartnerAction, CreatePartner, AuthorizeAction, - ChangelogAction, BlogAction, StaffPositionAction, StaffMemberAction, @@ -112,15 +100,7 @@ pub async fn init_panelapi(pool: PgPool, cache_http: botox::cache::CacheHttpImpl .await .expect("Failed to create staffpanel__authchain table"); - let cdn_file_chunks_cache = Cache::>::builder() - .time_to_live(Duration::from_secs(3600)) - .build(); - - let shared_state = Arc::new(AppState { - pool, - cache_http, - cdn_file_chunks_cache, - }); + let shared_state = Arc::new(AppState { pool, cache_http }); let app = Router::new() .route("/openapi", get(docs)) @@ -146,12 +126,6 @@ pub async fn init_panelapi(pool: PgPool, cache_http: botox::cache::CacheHttpImpl } } -/// CDN granularity: Check for [cdn].[permission] or [cdn#scope].[permission] -fn has_cdn_perm(user_perms: &[Permission], cdn_scope: &str, perm: &str) -> bool { - perms::has_perm(user_perms, &format!("cdn#{}.{}", cdn_scope, perm).into()) - || perms::has_perm(user_perms, &format!("cdn.{}", perm).into()) -} - /// Make Panel Query #[utoipa::path( post, @@ -177,83 +151,12 @@ async fn query( version, } => super::actions::hello::hello(&state, login_token, version).await, PanelQuery::BaseAnalytics { login_token } => { - super::auth::check_auth(&state.pool, &login_token) - .await - .map_err(Error::new)?; - - let bot_counts = sqlx::query!("SELECT type, COUNT(*) FROM bots GROUP BY type") - .fetch_all(&state.pool) - .await - .map_err(Error::new)?; - - let server_counts = sqlx::query!("SELECT type, COUNT(*) FROM servers GROUP BY type") - .fetch_all(&state.pool) - .await - .map_err(Error::new)?; - - let ticket_counts = sqlx::query!("SELECT open, COUNT(*) FROM tickets GROUP BY open") - .fetch_all(&state.pool) - .await - .map_err(Error::new)?; - - let total_users = sqlx::query!("SELECT COUNT(*) FROM users") - .fetch_one(&state.pool) - .await - .map_err(Error::new)?; - - let total_changelogs = sqlx::query!("SELECT COUNT(*) FROM changelogs") - .fetch_one(&state.pool) - .await - .map_err(Error::new)?; - - Ok(( - StatusCode::OK, - Json(BaseAnalytics { - bot_counts: bot_counts - .iter() - .map(|b| (b.r#type.clone(), b.count.unwrap_or_default())) - .collect(), - server_counts: server_counts - .iter() - .map(|s| (s.r#type.clone(), s.count.unwrap_or_default())) - .collect(), - ticket_counts: ticket_counts - .iter() - .map(|t| { - ( - if t.open { - "open".to_string() - } else { - "closed".to_string() - }, - t.count.unwrap_or_default(), - ) - }) - .collect(), - total_users: total_users.count.unwrap_or_default(), - changelogs_count: total_changelogs.count.unwrap_or_default(), - }), - ) - .into_response()) + super::actions::baseanalytics::base_analytics(&state, login_token).await } PanelQuery::GetUser { login_token, user_id, - } => { - super::auth::check_auth(&state.pool, &login_token) - .await - .map_err(Error::new)?; - - let user = crate::impls::dovewing::get_platform_user( - &state.pool, - DovewingSource::Discord(state.cache_http.clone()), - &user_id, - ) - .await - .map_err(Error::new)?; - - Ok((StatusCode::OK, Json(user)).into_response()) - } + } => super::actions::getuser::get_user(&state, login_token, user_id).await, PanelQuery::BotQueue { login_token } => { super::auth::check_auth(&state.pool, &login_token) .await @@ -376,2215 +279,60 @@ async fn query( } Ok((StatusCode::OK, Json(rpc_methods)).into_response()) - } - PanelQuery::GetRpcLogEntries { login_token } => { - let auth_data = super::auth::check_auth(&state.pool, &login_token) - .await - .map_err(Error::new)?; - - let user_perms = get_user_perms(&state.pool, &auth_data.user_id) - .await - .map_err(Error::new)? - .resolve(); - - if !perms::has_perm(&user_perms, &"rpc_logs.view".into()) { - return Ok(( - StatusCode::FORBIDDEN, - "You do not have permission to view rpc logs [rpc_logs.view]".to_string(), - ) - .into_response()); - } - - let entries = sqlx::query!( - "SELECT id, user_id, method, data, state, created_at FROM rpc_logs ORDER BY created_at DESC" - ) - .fetch_all(&state.pool) - .await - .map_err(Error::new)?; - - let mut rpc_log = vec![]; - - for entry in entries { - rpc_log.push(RPCLogEntry { - id: entry.id.to_string(), - user_id: entry.user_id, - method: entry.method, - data: entry.data, - state: entry.state, - created_at: entry.created_at, - }); - } - - Ok((StatusCode::OK, Json(rpc_log)).into_response()) - } - PanelQuery::SearchEntitys { - login_token, - target_type, - query, - } => { - super::auth::check_auth(&state.pool, &login_token) - .await - .map_err(Error::new)?; - - match target_type { - TargetType::Bot => { - let queue = sqlx::query!( - " - SELECT bot_id, client_id, type, approximate_votes, shards, library, invite_clicks, clicks, - servers, last_claimed, claimed_by, approval_note, short, invite FROM bots - INNER JOIN internal_user_cache__discord discord_users ON bots.bot_id = discord_users.id - WHERE bot_id = $1 OR client_id = $1 OR discord_users.username ILIKE $2 ORDER BY bots.created_at - ", - query, - format!("%{}%", query) - ) - .fetch_all(&state.pool) - .await - .map_err(Error::new)?; - - let mut bots = Vec::new(); - - for bot in queue { - let owners = crate::impls::utils::get_entity_managers( - TargetType::Bot, - &bot.bot_id, - &state.pool, - ) - .await - .map_err(Error::new)?; - - let user = crate::impls::dovewing::get_platform_user( - &state.pool, - DovewingSource::Discord(state.cache_http.clone()), - &bot.bot_id, - ) - .await - .map_err(Error::new)?; - - bots.push(PartialEntity::Bot(PartialBot { - bot_id: bot.bot_id, - client_id: bot.client_id, - user, - r#type: bot.r#type, - votes: bot.approximate_votes, - shards: bot.shards, - library: bot.library, - invite_clicks: bot.invite_clicks, - clicks: bot.clicks, - servers: bot.servers, - claimed_by: bot.claimed_by, - last_claimed: bot.last_claimed, - approval_note: bot.approval_note, - short: bot.short, - mentionable: owners.mentionables(), - invite: bot.invite, - })); - } - - Ok((StatusCode::OK, Json(bots)).into_response()) - } - TargetType::Server => { - let queue = sqlx::query!( - " - SELECT server_id, name, total_members, online_members, short, type, approximate_votes, invite_clicks, - clicks, nsfw, tags, premium, claimed_by, last_claimed FROM servers - WHERE server_id = $1 OR name ILIKE $2 ORDER BY created_at - ", - query, - format!("%{}%", query) - ) - .fetch_all(&state.pool) - .await - .map_err(Error::new)?; - - let mut servers = Vec::new(); - - for server in queue { - let owners = crate::impls::utils::get_entity_managers( - TargetType::Server, - &server.server_id, - &state.pool, - ) - .await - .map_err(Error::new)?; - - servers.push(PartialEntity::Server(PartialServer { - server_id: server.server_id.clone(), - name: server.name, - avatar: format!( - "{}/servers/avatars/{}.webp", - crate::config::CONFIG.cdn_url, - server.server_id - ), - total_members: server.total_members, - online_members: server.online_members, - short: server.short, - r#type: server.r#type, - votes: server.approximate_votes, - invite_clicks: server.invite_clicks, - clicks: server.clicks, - nsfw: server.nsfw, - tags: server.tags, - premium: server.premium, - claimed_by: server.claimed_by, - last_claimed: server.last_claimed, - mentionable: owners.mentionables(), - })); - } - - Ok((StatusCode::OK, Json(servers)).into_response()) - } - _ => Ok(( - StatusCode::NOT_IMPLEMENTED, - "Searching this target type is not implemented".to_string(), - ) - .into_response()), - } - } - PanelQuery::UploadCdnFileChunk { login_token, chunk } => { - let auth_data = super::auth::check_auth(&state.pool, &login_token) - .await - .map_err(Error::new)?; - - let user_perms = get_user_perms(&state.pool, &auth_data.user_id) - .await - .map_err(Error::new)? - .resolve(); - - if !perms::has_perm(&user_perms, &"cdn.upload_chunk".into()) { - return Ok(( - StatusCode::FORBIDDEN, - "You do not have permission to upload chunks to the CDN right now [cdn.upload_chunk]".to_string(), - ) - .into_response()); - } - - info!("Got chunk with len={}", chunk.len()); - - // Check that length of chunk is not greater than 100MB - if chunk.len() > 100_000_000 { - return Ok(( - StatusCode::BAD_REQUEST, - "Chunk size is too large".to_string(), - ) - .into_response()); - } - - // Check that chunk is not empty - if chunk.is_empty() { - return Ok((StatusCode::BAD_REQUEST, "Chunk is empty".to_string()).into_response()); - } - - // Create chunk ID - let chunk_id = botox::crypto::gen_random(32); - - // Keep looping until we get a free chunk ID - let mut tries = 0; - - while tries < 10 { - if !state.cdn_file_chunks_cache.contains_key(&chunk_id) { - state - .cdn_file_chunks_cache - .insert(chunk_id.clone(), chunk) - .await; - - return Ok((StatusCode::OK, chunk_id).into_response()); - } - - tries += 1; - } - - Ok(( - StatusCode::INTERNAL_SERVER_ERROR, - "Failed to generate a chunk ID".to_string(), - ) - .into_response()) - } - PanelQuery::ListCdnScopes { login_token } => { - let auth_data = super::auth::check_auth(&state.pool, &login_token) - .await - .map_err(Error::new)?; - - let user_perms = get_user_perms(&state.pool, &auth_data.user_id) - .await - .map_err(Error::new)? - .resolve(); - - if !perms::has_perm(&user_perms, &"cdn.list_scopes".into()) { - return Ok(( - StatusCode::FORBIDDEN, - "You do not have permission to list the CDN's scopes right now [cdn.list_scopes]".to_string(), - ) - .into_response()); - } - - Ok(( - StatusCode::OK, - Json(crate::config::CONFIG.panel.cdn_scopes.get().clone()), - ) - .into_response()) - } - PanelQuery::GetMainCdnScope { login_token } => { - super::auth::check_auth(&state.pool, &login_token) - .await - .map_err(Error::new)?; - - Ok(( - StatusCode::OK, - crate::config::CONFIG.panel.main_scope.clone(), - ) - .into_response()) - } - PanelQuery::UpdateCdnAsset { - login_token, - name, - path, - action, - cdn_scope, - } => { - let auth_data = super::auth::check_auth(&state.pool, &login_token) - .await - .map_err(Error::new)?; - - let user_perms = get_user_perms(&state.pool, &auth_data.user_id) - .await - .map_err(Error::new)? - .resolve(); - - // Get cdn path from cdn_scope hashmap - let cdn_scopes = crate::config::CONFIG.panel.cdn_scopes.get(); - let Some(cdn_path) = cdn_scopes.get(&cdn_scope) else { - return Ok( - (StatusCode::BAD_REQUEST, "Invalid CDN scope".to_string()).into_response() - ); - }; - - fn validate_name(name: &str) -> Result<(), crate::Error> { - const ALLOWED_CHARS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.:%$[](){}$@! "; - - // 1. Ensure all chars of name are in ALLOWED_CHARS - // 2. Ensure name does not contain a slash - // 3. Ensure name does not contain a backslash - // 4. Ensure name does not start with a dot - if name.chars().any(|c| !ALLOWED_CHARS.contains(c)) - || name.contains('/') - || name.contains('\\') - || name.starts_with('.') - { - return Err( - "Asset name cannot contain disallowed characters, slashes or backslashes or start with a dot" - .into(), - ); - } - - Ok(()) - } - - fn validate_path(path: &str) -> Result<(), crate::Error> { - const ALLOWED_CHARS: &str = - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.:%$/ "; - - // 1. Ensure all chars of name are in ALLOWED_CHARS - // 2. Ensure path does not contain a dot-dot (path escape) - // 3. Ensure path does not contain a double slash - // 4. Ensure path does not contain a backslash - // 5. Ensure path does not start with a slash - if path.chars().any(|c| !ALLOWED_CHARS.contains(c)) - || path.contains("..") - || path.contains("//") - || path.contains('\\') - || path.starts_with('/') - { - return Err("Asset path cannot contain non-ASCII characters, dot-dots, doubleslashes, backslashes or start with a slash".into()); - } - - Ok(()) - } - - validate_name(&name).map_err(Error::new)?; - validate_path(&path).map_err(Error::new)?; - - // Get asset path and final resolved path - let asset_path = if path.is_empty() { - cdn_path.path.to_string() - } else { - format!("{}/{}", cdn_path.path, path) - }; - - let asset_final_path = if name.is_empty() { - asset_path.clone() - } else { - format!("{}/{}", asset_path, name) - }; - - match action { - CdnAssetAction::ListPath => { - if !has_cdn_perm(&user_perms, &cdn_scope, "list") { - return Ok(( - StatusCode::FORBIDDEN, - "You do not have permission to list CDN assets right now [list]" - .to_string(), - ) - .into_response()); - } - - match std::fs::metadata(&asset_path) { - Ok(m) => { - if !m.is_dir() { - return Ok(( - StatusCode::BAD_REQUEST, - "Asset path already exists and is not a directory".to_string(), - ) - .into_response()); - } - } - Err(e) => { - return Ok(( - StatusCode::BAD_REQUEST, - "Fetching asset metadata failed: ".to_string() + &e.to_string(), - ) - .into_response()); - } - } - - let mut files = Vec::new(); - - for entry in std::fs::read_dir(&asset_path).map_err(Error::new)? { - let entry = entry.map_err(Error::new)?; - - let meta = entry.metadata().map_err(Error::new)?; - - let efn = entry.file_name(); - let Some(name) = efn.to_str() else { - continue; - }; - - files.push(CdnAssetItem { - name: name.to_string(), - path: entry - .path() - .to_str() - .unwrap_or_default() - .to_string() - .replace(&cdn_path.path, ""), - size: meta.len(), - last_modified: meta - .modified() - .map_err(Error::new)? - .duration_since(std::time::UNIX_EPOCH) - .map_err(Error::new)? - .as_secs(), - is_dir: meta.is_dir(), - permissions: meta.permissions().mode(), - }); - } - - Ok((StatusCode::OK, Json(files)).into_response()) - } - CdnAssetAction::ReadFile => { - if !has_cdn_perm(&user_perms, &cdn_scope, "read_file") { - return Ok(( - StatusCode::FORBIDDEN, - "You do not have permission to read CDN files right now [read_file]" - .to_string(), - ) - .into_response()); - } - - match std::fs::metadata(&asset_final_path) { - Ok(m) => { - if !m.is_file() { - return Ok(( - StatusCode::BAD_REQUEST, - "Asset path is not a file".to_string(), - ) - .into_response()); - } - } - Err(e) => { - return Ok(( - StatusCode::BAD_REQUEST, - "Fetching asset metadata failed: ".to_string() + &e.to_string(), - ) - .into_response()); - } - } - - let file = match tokio::fs::File::open(&asset_final_path).await { - Ok(file) => file, - Err(e) => { - return Ok(( - StatusCode::BAD_REQUEST, - "Reading file failed: ".to_string() + &e.to_string(), - ) - .into_response()); - } - }; - - let stream = tokio_util::io::ReaderStream::new(file); - let body = axum::body::Body::from_stream(stream); - - let headers = [(axum::http::header::CONTENT_TYPE, "application/octet-stream")]; - - Ok((headers, body).into_response()) - } - CdnAssetAction::CreateFolder => { - if !has_cdn_perm(&user_perms, &cdn_scope, "create_folder") { - return Ok(( - StatusCode::FORBIDDEN, - "You do not have permission to create CDN folders right now [create_folder]" - .to_string(), - ) - .into_response()); - } - - match std::fs::metadata(&asset_final_path) { - Ok(_) => { - return Ok(( - StatusCode::BAD_REQUEST, - "Asset path already exists".to_string(), - ) - .into_response()); - } - Err(e) => { - if e.kind() != std::io::ErrorKind::NotFound { - return Ok(( - StatusCode::BAD_REQUEST, - "Fetching asset metadata failed due to unknown error: " - .to_string() - + &e.to_string(), - ) - .into_response()); - } - } - } - - // Create path - std::fs::DirBuilder::new() - .recursive(true) - .create(&asset_final_path) - .map_err(Error::new)?; - - Ok((StatusCode::NO_CONTENT, "").into_response()) - } - CdnAssetAction::AddFile { - overwrite, - chunks, - sha512, - } => { - if !has_cdn_perm(&user_perms, &cdn_scope, "add_file") { - return Ok(( - StatusCode::FORBIDDEN, - "You do not have permission to add CDN files right now [cdn.add_file]" - .to_string(), - ) - .into_response()); - } - - if chunks.is_empty() { - return Ok(( - StatusCode::BAD_REQUEST, - "No chunks were provided".to_string(), - ) - .into_response()); - } - - if chunks.len() > 100_000 { - return Ok(( - StatusCode::BAD_REQUEST, - "Too many chunks were provided".to_string(), - ) - .into_response()); - } - - for chunk in &chunks { - if !state.cdn_file_chunks_cache.contains_key(chunk) { - return Ok(( - StatusCode::BAD_REQUEST, - "Chunk does not exist".to_string(), - ) - .into_response()); - } - } - - // Check if the asset exists - match std::fs::metadata(&asset_final_path) { - Ok(m) => { - if overwrite { - if m.is_dir() { - return Ok(( - StatusCode::BAD_REQUEST, - "Asset to be replaced is a directory".to_string(), - ) - .into_response()); - } - } else { - return Ok(( - StatusCode::BAD_REQUEST, - "Asset already exists".to_string(), - ) - .into_response()); - } - } - Err(e) => { - if e.kind() != std::io::ErrorKind::NotFound { - return Ok(( - StatusCode::BAD_REQUEST, - "Fetching asset metadata failed due to unknown error: " - .to_string() - + &e.to_string(), - ) - .into_response()); - } - } - } - - match std::fs::metadata(&asset_path) { - Ok(m) => { - if !m.is_dir() { - return Ok(( - StatusCode::BAD_REQUEST, - "Asset path already exists and is not a directory".to_string(), - ) - .into_response()); - } - } - Err(e) => { - if e.kind() != std::io::ErrorKind::NotFound { - return Ok(( - StatusCode::BAD_REQUEST, - "Fetching asset metadata failed due to unknown error: " - .to_string() - + &e.to_string(), - ) - .into_response()); - } else { - // Create path - std::fs::DirBuilder::new() - .recursive(true) - .create(&asset_path) - .map_err(Error::new)?; - } - } - } - - { - let tmp_file_path = format!( - "/tmp/arcadia-cdn-file{}@{}", - botox::crypto::gen_random(32), - asset_final_path.replace('/', ">") - ); - - let mut temp_file = tokio::fs::File::create(&tmp_file_path) - .await - .map_err(Error::new)?; - - // For each chunk, fetch and add to file - for chunk in chunks { - let chunk = state - .cdn_file_chunks_cache - .remove(&chunk) - .await - .ok_or_else(|| { - Error::new("Chunk ".to_string() + &chunk + " does not exist") - })?; - - temp_file.write_all(&chunk).await.map_err(Error::new)?; - } - - // Sync file - temp_file.sync_all().await.map_err(Error::new)?; - - // Close file - drop(temp_file); - - // Calculate sha512 of file - let mut hasher = Sha512::new(); - - let mut file = tokio::fs::File::open(&tmp_file_path) - .await - .map_err(Error::new)?; - - let mut file_buf = Vec::new(); - file.read_to_end(&mut file_buf).await.map_err(Error::new)?; - - hasher.update(&file_buf); - - let hash = hasher.finalize(); - - let hash_expected = data_encoding::HEXLOWER.encode(&hash); - - if sha512 != hash_expected { - return Ok(( - StatusCode::BAD_REQUEST, - "SHA512 hash does not match".to_string(), - ) - .into_response()); - } - - // Rename temp file to final path - tokio::fs::copy(&tmp_file_path, &asset_final_path) - .await - .map_err(Error::new)?; - - // Delete temp file - tokio::fs::remove_file(&tmp_file_path) - .await - .map_err(Error::new)?; - } - - Ok((StatusCode::NO_CONTENT, "").into_response()) - } - CdnAssetAction::CopyFile { - overwrite, - delete_original, - copy_to, - } => { - if !has_cdn_perm(&user_perms, &cdn_scope, "copy_file") { - return Ok(( - StatusCode::FORBIDDEN, - "You do not have permission to copy files right now [copy_file]" - .to_string(), - ) - .into_response()); - } - - validate_path(©_to).map_err(Error::new)?; - - let copy_to = if copy_to.is_empty() { - return Ok(( - StatusCode::BAD_REQUEST, - "copy_to location cannot be empty".to_string(), - ) - .into_response()); - } else { - format!("{}/{}", cdn_path.path, copy_to) - }; - - match std::fs::metadata(©_to) { - Ok(m) => { - if !m.is_dir() && !overwrite { - return Ok(( - StatusCode::BAD_REQUEST, - "copy_to location already exists".to_string(), - ) - .into_response()); - } - } - Err(e) => { - if e.kind() != std::io::ErrorKind::NotFound { - return Ok(( - StatusCode::BAD_REQUEST, - "Fetching asset metadata failed due to unknown error: " - .to_string() - + &e.to_string(), - ) - .into_response()); - } - } - } - - match std::fs::metadata(&asset_final_path) { - Ok(m) => { - if m.is_symlink() || m.is_file() { - if delete_original { - // This is just a rename operation - std::fs::rename(&asset_final_path, ©_to).map_err(|e| { - Error::new(format!( - "Failed to rename file: {} from {} to {}", - e, &asset_final_path, ©_to - )) - })?; - } else { - // This is a copy operation - std::fs::copy(&asset_final_path, ©_to) - .map_err(Error::new)?; - } - } else if m.is_dir() { - if delete_original { - // This is a rename operation - fn rename_dir_all( - src: impl AsRef, - dst: impl AsRef, - ) -> std::io::Result<()> { - std::fs::create_dir_all(&dst)?; - for entry in std::fs::read_dir(src)? { - let entry = entry?; - let ty = entry.file_type()?; - if ty.is_dir() { - rename_dir_all( - entry.path(), - dst.as_ref().join(entry.file_name()), - )?; - } else { - std::fs::rename( - entry.path(), - dst.as_ref().join(entry.file_name()), - )?; - } - } - Ok(()) - } - - rename_dir_all(&asset_final_path, ©_to) - .map_err(Error::new)?; - - // Delete original directory - std::fs::remove_dir_all(&asset_final_path) - .map_err(Error::new)?; - } else { - // This is a recursive copy operation - fn copy_dir_all( - src: impl AsRef, - dst: impl AsRef, - ) -> std::io::Result<()> { - std::fs::create_dir_all(&dst)?; - for entry in std::fs::read_dir(src)? { - let entry = entry?; - let ty = entry.file_type()?; - if ty.is_dir() { - copy_dir_all( - entry.path(), - dst.as_ref().join(entry.file_name()), - )?; - } else { - std::fs::copy( - entry.path(), - dst.as_ref().join(entry.file_name()), - )?; - } - } - Ok(()) - } - - copy_dir_all(&asset_final_path, ©_to) - .map_err(Error::new)?; - } - } - } - Err(e) => { - return Ok(( - StatusCode::BAD_REQUEST, - "Could not find asset: ".to_string() - + &e.to_string() - + &format!(" (path: {})", &asset_final_path), - ) - .into_response()); - } - } - - Ok((StatusCode::NO_CONTENT, "").into_response()) - } - CdnAssetAction::Delete => { - if !has_cdn_perm(&user_perms, &cdn_scope, "delete") { - return Ok(( - StatusCode::FORBIDDEN, - "You do not have permission to delete CDN assets right now [delete]" - .to_string(), - ) - .into_response()); - } - - // Check if the asset exists - match std::fs::metadata(&asset_final_path) { - Ok(m) => { - if m.is_symlink() || m.is_file() { - // Delete the symlink - std::fs::remove_file(asset_final_path).map_err(Error::new)?; - } else if m.is_dir() { - // Delete the directory - std::fs::remove_dir_all(asset_final_path).map_err(Error::new)?; - } - - Ok((StatusCode::NO_CONTENT, "").into_response()) - } - Err(e) => Ok(( - StatusCode::BAD_REQUEST, - "Could not find asset: ".to_string() + &e.to_string(), - ) - .into_response()), - } - } - CdnAssetAction::PersistGit { - message, - current_dir, - } => { - if !has_cdn_perm(&user_perms, &cdn_scope, "persist_git") { - return Ok(( - StatusCode::FORBIDDEN, - "You do not have permission to persist CDN git right now [cdn.persist_git]" - .to_string(), - ) - .into_response()); - } - - let mut cmd_output = indexmap::IndexMap::new(); - - // Use git rev-parse --show-toplevel to get the root of the repo - let output = tokio::process::Command::new("git") - .arg("rev-parse") - .arg("--show-toplevel") - .current_dir(&asset_final_path) - .output() - .await - .map_err(|e| { - Error::new(format!("Failed to execute git rev-parse: {}", e)) - })?; - - let repo_root = std::str::from_utf8(&output.stdout) - .map_err(|e| Error::new(format!("Failed to parse git output: {}", e)))? - .trim() - .replace('\n', "") - .to_string(); - - cmd_output.insert("git rev-parse --show-toplevel", repo_root.clone()); - - if !output.status.success() { - cmd_output.insert("error", output.status.to_string()); - return Ok((StatusCode::OK, Json(cmd_output)).into_response()); - } - - // If current_dir is set, then set curr dir to that - // - // Otherwise, set curr dir to the root of the repo - let curr_dir = if !current_dir { - repo_root.clone() - } else { - asset_final_path.clone() - }; - - cmd_output.insert("[dir]", curr_dir.clone()); - - // Run `git add .` in the current directory - let output = tokio::process::Command::new("git") - .arg("add") - .arg(".") - .current_dir(&curr_dir) - .env("GIT_TERMINAL_PROMPT", "0") - .output() - .await - .map_err(|e| Error::new(format!("Failed to execute git add: {}", e)))?; - - cmd_output.insert( - "git add", - std::str::from_utf8(&output.stdout) - .map_err(|e| Error::new(format!("Failed to parse git output: {}", e)))? - .trim() - .to_string(), - ); - - if !output.status.success() { - cmd_output.insert("error", output.status.to_string()); - return Ok((StatusCode::OK, Json(cmd_output)).into_response()); - } - - // Check if theres already a pending commit - - // Run `git commit -m ` in the current directory - let output = tokio::process::Command::new("git") - .arg("commit") - .arg("-m") - .arg(message) - .env("GIT_TERMINAL_PROMPT", "0") - .current_dir(&curr_dir) - .output() - .await - .map_err(|e| Error::new(format!("Failed to execute git commit: {}", e)))?; - - cmd_output.insert( - "git commit", - std::str::from_utf8(&output.stdout) - .map_err(|e| Error::new(format!("Failed to parse git output: {}", e)))? - .trim() - .to_string(), - ); - - if !output.status.success() { - cmd_output.insert("error_gc", output.status.to_string()); - } - - // Run `git push --force` in the current directory - let output = tokio::process::Command::new("git") - .arg("push") - .arg("--force") - .env("GIT_TERMINAL_PROMPT", "0") - .current_dir(&curr_dir) - .output() - .await - .map_err(|e| Error::new(format!("Failed to execute git push: {}", e)))?; - - cmd_output.insert( - "git push", - std::str::from_utf8(&output.stdout) - .map_err(|e| Error::new(format!("Failed to parse git output: {}", e)))? - .trim() - .to_string(), - ); - - if !output.status.success() { - cmd_output.insert("error_gp", output.status.to_string()); - return Ok((StatusCode::OK, Json(cmd_output)).into_response()); - } - - Ok((StatusCode::OK, Json(cmd_output)).into_response()) - } - } - } - PanelQuery::UpdatePartners { - login_token, - action, - } => { - let auth_data = super::auth::check_auth(&state.pool, &login_token) - .await - .map_err(Error::new)?; - - let user_perms = get_user_perms(&state.pool, &auth_data.user_id) - .await - .map_err(Error::new)? - .resolve(); - - async fn parse_partner( - pool: &PgPool, - partner: &CreatePartner, - ) -> Result<(), crate::Error> { - // Check if partner type exists - let partner_type_exists = - sqlx::query!("SELECT id FROM partner_types WHERE id = $1", partner.r#type) - .fetch_optional(pool) - .await? - .is_some(); - - if !partner_type_exists { - return Err("Partner type does not exist".into()); - } - - // Ensure that image has been uploaded to CDN - // Get cdn path from cdn_scope hashmap - let cdn_scopes = crate::config::CONFIG.panel.cdn_scopes.get(); - - let Some(cdn_path) = cdn_scopes.get(&crate::config::CONFIG.panel.main_scope) else { - return Err("Main scope not found".into()); - }; - - let path = format!("{}/avatars/partners/{}.webp", cdn_path.path, partner.id); - - match std::fs::metadata(&path) { - Ok(m) => { - if !m.is_file() { - return Err("Image does not exist".into()); - } - - if m.len() > 100_000_000 { - return Err("Image is too large".into()); - } - - if m.len() == 0 { - return Err("Image is empty".into()); - } - } - Err(e) => { - return Err(("Fetching image metadata failed: ".to_string() - + &e.to_string()) - .into()); - } - }; - - if partner.links.is_empty() { - return Err("Links cannot be empty".into()); - } - - for link in &partner.links { - if link.name.is_empty() { - return Err("Link name cannot be empty".into()); - } - - if link.value.is_empty() { - return Err("Link URL cannot be empty".into()); - } - - if !link.value.starts_with("https://") { - return Err("Link URL must start with https://".into()); - } - } - - // Check user id - let user_exists = sqlx::query!( - "SELECT user_id FROM users WHERE user_id = $1", - partner.user_id - ) - .fetch_optional(pool) - .await? - .is_some(); - - if !user_exists { - return Err("User does not exist".into()); - } - - Ok(()) - } - - match action { - PartnerAction::List => { - let prec = sqlx::query!( - "SELECT id, name, short, links, type, created_at, user_id, bot_id FROM partners" - ) - .fetch_all(&state.pool) - .await - .map_err(Error::new)?; - - let mut partners = Vec::new(); - - for partner in prec { - partners.push(Partner { - id: partner.id, - name: partner.name, - short: partner.short, - links: serde_json::from_value(partner.links).map_err(Error::new)?, - r#type: partner.r#type, - created_at: partner.created_at, - user_id: partner.user_id, - bot_id: partner.bot_id, - }) - } - - let ptrec = - sqlx::query!("SELECT id, name, short, icon, created_at FROM partner_types") - .fetch_all(&state.pool) - .await - .map_err(Error::new)?; - - let mut partner_types = Vec::new(); - - for partner_type in ptrec { - partner_types.push(PartnerType { - id: partner_type.id, - name: partner_type.name, - short: partner_type.short, - icon: partner_type.icon, - created_at: partner_type.created_at, - }) - } - - Ok(( - StatusCode::OK, - Json(Partners { - partners, - partner_types, - }), - ) - .into_response()) - } - PartnerAction::Create { partner } => { - if !perms::has_perm(&user_perms, &"partners.create".into()) { - return Ok(( - StatusCode::FORBIDDEN, - "You do not have permission to create partners [partners.create]" - .to_string(), - ) - .into_response()); - } - - // Check if partner already exists - let partner_exists = - sqlx::query!("SELECT id FROM partners WHERE id = $1", partner.id) - .fetch_optional(&state.pool) - .await - .map_err(Error::new)? - .is_some(); - - if partner_exists { - return Ok(( - StatusCode::BAD_REQUEST, - "Partner already exists".to_string(), - ) - .into_response()); - } - - if let Err(e) = parse_partner(&state.pool, &partner).await { - return Ok((StatusCode::BAD_REQUEST, e.to_string()).into_response()); - } - - // Insert partner - sqlx::query!( - "INSERT INTO partners (id, name, short, links, type, user_id, bot_id) VALUES ($1, $2, $3, $4, $5, $6, $7)", - partner.id, - partner.name, - partner.short, - serde_json::to_value(partner.links).map_err(Error::new)?, - partner.r#type, - partner.user_id, - partner.bot_id - ) - .execute(&state.pool) - .await - .map_err(Error::new)?; - - Ok((StatusCode::NO_CONTENT, "").into_response()) - } - PartnerAction::Update { partner } => { - if !perms::has_perm(&user_perms, &"partners.update".into()) { - return Ok(( - StatusCode::FORBIDDEN, - "You do not have permission to update partners [partners.update]" - .to_string(), - ) - .into_response()); - } - - // Check if partner already exists - let partner_exists = - sqlx::query!("SELECT id FROM partners WHERE id = $1", partner.id) - .fetch_optional(&state.pool) - .await - .map_err(Error::new)? - .is_some(); - - if !partner_exists { - return Ok(( - StatusCode::BAD_REQUEST, - "Partner does not already exist".to_string(), - ) - .into_response()); - } - - if let Err(e) = parse_partner(&state.pool, &partner).await { - return Ok((StatusCode::BAD_REQUEST, e.to_string()).into_response()); - } - - // Update partner - sqlx::query!( - "UPDATE partners SET name = $2, short = $3, links = $4, type = $5, user_id = $6, bot_id = $7 WHERE id = $1", - partner.id, - partner.name, - partner.short, - serde_json::to_value(partner.links).map_err(Error::new)?, - partner.r#type, - partner.user_id, - partner.bot_id - ) - .execute(&state.pool) - .await - .map_err(Error::new)?; - - Ok((StatusCode::NO_CONTENT, "").into_response()) - } - PartnerAction::Delete { id } => { - if !perms::has_perm(&user_perms, &"partners.delete".into()) { - return Ok(( - StatusCode::FORBIDDEN, - "You do not have permission to delete partners [partners.delete]" - .to_string(), - ) - .into_response()); - } - - // Check if partner exists - let partner_exists = sqlx::query!("SELECT id FROM partners WHERE id = $1", id) - .fetch_optional(&state.pool) - .await - .map_err(Error::new)? - .is_some(); - - if !partner_exists { - return Ok(( - StatusCode::BAD_REQUEST, - "Partner does not exist".to_string(), - ) - .into_response()); - } - - // Ensure that image has been uploaded to CDN - // Get cdn path from cdn_scope hashmap - let cdn_scopes = crate::config::CONFIG.panel.cdn_scopes.get(); - - let Some(cdn_path) = cdn_scopes.get(&crate::config::CONFIG.panel.main_scope) - else { - return Ok( - (StatusCode::BAD_REQUEST, "Main scope not found".to_string()) - .into_response(), - ); - }; - - let path = format!("{}/partners/{}.webp", cdn_path.path, id); - - match std::fs::metadata(&path) { - Ok(m) => { - if m.is_symlink() || m.is_file() { - // Delete the symlink - std::fs::remove_file(path).map_err(Error::new)?; - } else if m.is_dir() { - // Delete the directory - std::fs::remove_dir_all(path).map_err(Error::new)?; - } - } - Err(e) => { - if e.kind() != std::io::ErrorKind::NotFound { - return Ok(( - StatusCode::BAD_REQUEST, - "Fetching asset metadata failed due to unknown error: " - .to_string() - + &e.to_string(), - ) - .into_response()); - } - } - }; - - sqlx::query!("DELETE FROM partners WHERE id = $1", id) - .execute(&state.pool) - .await - .map_err(Error::new)?; - - Ok((StatusCode::NO_CONTENT, "").into_response()) - } - } - } - PanelQuery::UpdateChangelog { - login_token, - action, - } => { - let auth_data = super::auth::check_auth(&state.pool, &login_token) - .await - .map_err(Error::new)?; - - let user_perms = get_user_perms(&state.pool, &auth_data.user_id) - .await - .map_err(Error::new)? - .resolve(); - - match action { - ChangelogAction::ListEntries => { - let rows = sqlx::query!( - "SELECT version, added, updated, removed, github_html, created_at, extra_description, prerelease, published FROM changelogs ORDER BY version::semver DESC" - ) - .fetch_all(&state.pool) - .await - .map_err(Error::new)?; - - let mut entries = Vec::new(); - - for row in rows { - entries.push(ChangelogEntry { - version: row.version, - added: row.added, - updated: row.updated, - removed: row.removed, - github_html: row.github_html, - created_at: row.created_at, - extra_description: row.extra_description, - prerelease: row.prerelease, - published: row.published, - }); - } - - Ok((StatusCode::OK, Json(entries)).into_response()) - } - ChangelogAction::CreateEntry { - version, - extra_description, - prerelease, - added, - updated, - removed, - } => { - if !perms::has_perm(&user_perms, &"changelogs.create".into()) { - return Ok(( - StatusCode::FORBIDDEN, - "You do not have permission to create changelog entries [changelogs.create]" - .to_string(), - ) - .into_response()); - } - - // Check if entry already exists with same vesion - if sqlx::query!( - "SELECT COUNT(*) FROM changelogs WHERE version = $1", - version - ) - .fetch_one(&state.pool) - .await - .map_err(Error::new)? - .count - .unwrap_or(0) - > 0 - { - return Ok(( - StatusCode::BAD_REQUEST, - "Entry with same version already exists".to_string(), - ) - .into_response()); - } - - // Insert entry - sqlx::query!( - "INSERT INTO changelogs (version, extra_description, prerelease, added, updated, removed) VALUES ($1, $2, $3, $4, $5, $6)", - version, - extra_description, - prerelease, - &added, - &updated, - &removed, - ) - .execute(&state.pool) - .await - .map_err(Error::new)?; - - Ok((StatusCode::NO_CONTENT, "").into_response()) - } - ChangelogAction::UpdateEntry { - version, - extra_description, - github_html, - prerelease, - added, - updated, - removed, - published, - } => { - if !perms::has_perm(&user_perms, &"changelogs.update".into()) { - return Ok(( - StatusCode::FORBIDDEN, - "You do not have permission to update changelog entries [changelogs.update]" - .to_string(), - ) - .into_response()); - } - - // Check if entry already exists with same vesion - if sqlx::query!( - "SELECT COUNT(*) FROM changelogs WHERE version = $1", - version - ) - .fetch_one(&state.pool) - .await - .map_err(Error::new)? - .count - .unwrap_or(0) - == 0 - { - return Ok(( - StatusCode::BAD_REQUEST, - "Entry with same version does not already exist".to_string(), - ) - .into_response()); - } - - // Update entry - sqlx::query!( - "UPDATE changelogs SET extra_description = $2, github_html = $3, prerelease = $4, added = $5, updated = $6, removed = $7, published = $8 WHERE version = $1", - version, - extra_description, - github_html, - prerelease, - &added, - &updated, - &removed, - published - ) - .execute(&state.pool) - .await - .map_err(Error::new)?; - - Ok((StatusCode::NO_CONTENT, "").into_response()) - } - ChangelogAction::DeleteEntry { version } => { - if !perms::has_perm(&user_perms, &"changelogs.delete".into()) { - return Ok(( - StatusCode::FORBIDDEN, - "You do not have permission to delete changelog entries [changelogs.delete]" - .to_string(), - ) - .into_response()); - } - - // Check if entry already exists with same vesion - if sqlx::query!( - "SELECT COUNT(*) FROM changelogs WHERE version = $1", - version - ) - .fetch_one(&state.pool) - .await - .map_err(Error::new)? - .count - .unwrap_or(0) - == 0 - { - return Ok(( - StatusCode::BAD_REQUEST, - "Entry with same version does not already exist".to_string(), - ) - .into_response()); - } - - // Delete entry - sqlx::query!("DELETE FROM changelogs WHERE version = $1", version) - .execute(&state.pool) - .await - .map_err(Error::new)?; - - Ok((StatusCode::NO_CONTENT, "").into_response()) - } - } - } - PanelQuery::UpdateBlog { - login_token, - action, - } => { - let auth_data = super::auth::check_auth(&state.pool, &login_token) - .await - .map_err(Error::new)?; - - let user_perms = get_user_perms(&state.pool, &auth_data.user_id) - .await - .map_err(Error::new)? - .resolve(); - - // TODO: Make this not require a wasteful query - let ad = super::auth::check_auth(&state.pool, &login_token) - .await - .map_err(Error::new)?; - - match action { - BlogAction::ListEntries => { - let rows = sqlx::query!( - "SELECT itag, slug, title, description, user_id, content, created_at, draft, tags FROM blogs ORDER BY created_at DESC" - ) - .fetch_all(&state.pool) - .await - .map_err(Error::new)?; - - let mut entries = Vec::new(); - - for row in rows { - entries.push(BlogPost { - itag: row.itag.hyphenated().to_string(), - slug: row.slug, - title: row.title, - description: row.description, - user_id: row.user_id, - tags: row.tags, - content: row.content, - created_at: row.created_at, - draft: row.draft, - }); - } - - Ok((StatusCode::OK, Json(entries)).into_response()) - } - BlogAction::CreateEntry { - slug, - title, - description, - content, - tags, - } => { - if !perms::has_perm(&user_perms, &"blog.create_entry".into()) { - return Ok(( - StatusCode::FORBIDDEN, - "You do not have permission to create blog entries [blog.create_entry]" - .to_string(), - ) - .into_response()); - } - - // Insert entry - sqlx::query!( - "INSERT INTO blogs (slug, title, description, content, tags, user_id) VALUES ($1, $2, $3, $4, $5, $6)", - slug, - title, - description, - content, - &tags, - &ad.user_id, - ) - .execute(&state.pool) - .await - .map_err(Error::new)?; - - Ok((StatusCode::NO_CONTENT, "").into_response()) - } - BlogAction::UpdateEntry { - itag, - slug, - title, - description, - content, - tags, - draft, - } => { - if !perms::has_perm(&user_perms, &"blog.update_entry".into()) { - return Ok(( - StatusCode::FORBIDDEN, - "You do not have permission to update blog entries [blog.update_entry]" - .to_string(), - ) - .into_response()); - } - - let uuid = sqlx::types::uuid::Uuid::parse_str(&itag).map_err(Error::new)?; - - // Check if entry already exists with same vesion - if sqlx::query!("SELECT COUNT(*) FROM blogs WHERE itag = $1", uuid) - .fetch_one(&state.pool) - .await - .map_err(Error::new)? - .count - .unwrap_or(0) - == 0 - { - return Ok( - (StatusCode::BAD_REQUEST, "Entry does not exist".to_string()) - .into_response(), - ); - } - - // Update entry - sqlx::query!( - "UPDATE blogs SET slug = $2, title = $3, description = $4, content = $5, tags = $6, draft = $7 WHERE itag = $1", - uuid, - slug, - title, - description, - content, - &tags, - draft - ) - .execute(&state.pool) - .await - .map_err(Error::new)?; - - Ok((StatusCode::NO_CONTENT, "").into_response()) - } - BlogAction::DeleteEntry { itag } => { - if !perms::has_perm(&user_perms, &"blog.delete_entry".into()) { - return Ok(( - StatusCode::FORBIDDEN, - "You do not have permission to delete blog entries [blog.delete_entry]" - .to_string(), - ) - .into_response()); - } - - // Check if entry already exists with same vesion - let uuid = sqlx::types::uuid::Uuid::parse_str(&itag).map_err(Error::new)?; - if sqlx::query!("SELECT COUNT(*) FROM blogs WHERE itag = $1", uuid) - .fetch_one(&state.pool) - .await - .map_err(Error::new)? - .count - .unwrap_or(0) - == 0 - { - return Ok(( - StatusCode::BAD_REQUEST, - "Entry with same id does not already exist".to_string(), - ) - .into_response()); - } - - // Delete entry - sqlx::query!("DELETE FROM blogs WHERE itag = $1", uuid) - .execute(&state.pool) - .await - .map_err(Error::new)?; - - Ok((StatusCode::NO_CONTENT, "").into_response()) - } - } - } - PanelQuery::UpdateStaffPositions { - login_token, - action, - } => { - let auth_data = super::auth::check_auth(&state.pool, &login_token) - .await - .map_err(Error::new)?; - - match action { - StaffPositionAction::ListPositions => { - let pos = sqlx::query!("SELECT id, name, role_id, perms, corresponding_roles, icon, index, created_at FROM staff_positions ORDER BY index ASC") - .fetch_all(&state.pool) - .await - .map_err(|e| format!("Error while getting staff positions {}", e)) - .map_err(Error::new)?; - - let mut positions = Vec::new(); - - for position_data in pos { - positions.push(StaffPosition { - id: position_data.id.hyphenated().to_string(), - name: position_data.name, - role_id: position_data.role_id, - perms: position_data.perms, - corresponding_roles: serde_json::from_value( - position_data.corresponding_roles, - ) - .map_err(Error::new)?, - icon: position_data.icon, - index: position_data.index, - created_at: position_data.created_at, - }); - } - - Ok((StatusCode::OK, Json(positions)).into_response()) - } - StaffPositionAction::SwapIndex { a, b } => { - // Get permissions - let sm = super::auth::get_staff_member( - &state.pool, - &state.cache_http, - &auth_data.user_id, - ) - .await - .map_err(Error::new)?; - - if !perms::has_perm(&sm.resolved_perms, &"staff_positions.swap_index".into()) { - return Ok(( - StatusCode::FORBIDDEN, - "You do not have permission to swap indexes of staff positions [staff_positions.swap_index]".to_string(), - ) - .into_response()); - } - - // Get the lowest index permission of the member - let mut sm_lowest_index = i32::MAX; - - for perm in &sm.positions { - if perm.index < sm_lowest_index { - sm_lowest_index = perm.index; - } - } - - let mut tx = state.pool.begin().await.map_err(Error::new)?; - - let index_a = - sqlx::query!("SELECT index FROM staff_positions WHERE id::text = $1", a) - .fetch_one(&mut *tx) - .await - .map_err(|e| format!("Error while getting lower position {}", e)) - .map_err(Error::new)? - .index; - - // Get the higher staff positions index - let index_b = - sqlx::query!("SELECT index FROM staff_positions WHERE id::text = $1", b) - .fetch_one(&mut *tx) - .await - .map_err(|e| format!("Error while getting higher position {}", e)) - .map_err(Error::new)? - .index; - - if index_a == index_b { - return Ok(( - StatusCode::BAD_REQUEST, - "Positions have the same index".to_string(), - ) - .into_response()); - } - - // If either a or b is lower than the lowest index of the member, then error - if index_a <= sm_lowest_index || index_b <= sm_lowest_index { - return Ok(( - StatusCode::FORBIDDEN, - "Either 'a' or 'b' is lower than the lowest index of the member" - .to_string(), - ) - .into_response()); - } - - // Swap the indexes - sqlx::query!( - "UPDATE staff_positions SET index = $1 WHERE id::text = $2", - index_b, - a - ) - .execute(&mut *tx) - .await - .map_err(|e| format!("Error while updating lower position {}", e)) - .map_err(Error::new)?; - - sqlx::query!( - "UPDATE staff_positions SET index = $1 WHERE id::text = $2", - index_a, - b - ) - .execute(&mut *tx) - .await - .map_err(|e| format!("Error while updating higher position {}", e)) - .map_err(Error::new)?; - - tx.commit().await.map_err(Error::new)?; - - Ok((StatusCode::NO_CONTENT, "").into_response()) - } - StaffPositionAction::SetIndex { id, index } => { - let uuid = sqlx::types::uuid::Uuid::parse_str(&id).map_err(Error::new)?; - - // Get permissions - let sm = super::auth::get_staff_member( - &state.pool, - &state.cache_http, - &auth_data.user_id, - ) - .await - .map_err(Error::new)?; - - if !perms::has_perm(&sm.resolved_perms, &"staff_positions.set_index".into()) { - return Ok(( - StatusCode::FORBIDDEN, - "You do not have permission to set the indexes of staff positions [staff_positions.set_index]".to_string(), - ) - .into_response()); - } - - if index < 0 { - return Ok(( - StatusCode::BAD_REQUEST, - "Index cannot be lower than 0".to_string(), - ) - .into_response()); - } - - // Get the lowest index permission of the member - let mut sm_lowest_index = i32::MAX; - - for perm in &sm.positions { - if perm.index < sm_lowest_index { - sm_lowest_index = perm.index; - } - } - - if index <= sm_lowest_index { - return Ok(( - StatusCode::FORBIDDEN, - "Index to set is lower than or equal to the lowest index of the staff member".to_string(), - ) - .into_response()); - } - - let mut tx = state.pool.begin().await.map_err(Error::new)?; - - let curr_index = - sqlx::query!("SELECT index FROM staff_positions WHERE id = $1", uuid) - .fetch_one(&mut *tx) - .await - .map_err(|e| format!("Error while getting position {}", e)) - .map_err(Error::new)? - .index; - - // If the current index is lower than the lowest index of the member, then error - if curr_index <= sm_lowest_index { - return Ok(( - StatusCode::FORBIDDEN, - "Current index of position is lower than or equal to the lowest index of the staff member".to_string(), - ) - .into_response()); - } - - // Shift indexes one lower - sqlx::query!( - "UPDATE staff_positions SET index = index + 1 WHERE index >= $1", - index - ) - .execute(&mut *tx) - .await - .map_err(|e| format!("Error while shifting indexes {}", e)) - .map_err(Error::new)?; - - // Set the index - sqlx::query!( - "UPDATE staff_positions SET index = $1 WHERE id = $2", - index, - uuid - ) - .execute(&mut *tx) - .await - .map_err(|e| format!("Error while updating position {}", e)) - .map_err(Error::new)?; - - tx.commit().await.map_err(Error::new)?; - - Ok((StatusCode::NO_CONTENT, "").into_response()) - } - StaffPositionAction::CreatePosition { - name, - role_id, - perms, - index, - corresponding_roles, - icon, - } => { - // Get permissions - let sm = super::auth::get_staff_member( - &state.pool, - &state.cache_http, - &auth_data.user_id, - ) - .await - .map_err(Error::new)?; - - if !perms::has_perm(&sm.resolved_perms, &"staff_positions.create".into()) { - return Ok(( - StatusCode::FORBIDDEN, - "You do not have permission to create staff positions [staff_positions.create]".to_string(), - ) - .into_response()); - } - - if index < 0 { - return Ok(( - StatusCode::BAD_REQUEST, - "Index cannot be lower than 0".to_string(), - ) - .into_response()); - } - - // Get the lowest index permission of the member - let mut sm_lowest_index = i32::MAX; - - for perm in &sm.positions { - if perm.index < sm_lowest_index { - sm_lowest_index = perm.index; - } - } - - if index <= sm_lowest_index { - return Ok(( - StatusCode::FORBIDDEN, - "Index is lower than or equal to the lowest index of the staff member" - .to_string(), - ) - .into_response()); - } - - // Shift indexes one lower - let mut tx = state.pool.begin().await.map_err(Error::new)?; - sqlx::query!( - "UPDATE staff_positions SET index = index + 1 WHERE index >= $1", - index - ) - .execute(&mut *tx) - .await - .map_err(|e| format!("Error while shifting indexes {}", e)) - .map_err(Error::new)?; - - // Ensure role id exists on the staff server - let role_id_snow = role_id.parse::().map_err(Error::new)?; - let role_exists = { - let guild = state - .cache_http - .cache - .guild(crate::config::CONFIG.servers.staff); - - if let Some(guild) = guild { - guild.roles.get(&role_id_snow).is_some() - } else { - false - } - }; - - if !role_exists { - return Ok(( - StatusCode::BAD_REQUEST, - "Role does not exist on the staff server".to_string(), - ) - .into_response()); - } - - // Ensure all corresponding_roles exist on the named server if - for role in corresponding_roles.iter() { - let Ok(corr_server) = CorrespondingServer::from_str(&role.name) else { - return Ok(( - StatusCode::BAD_REQUEST, - format!("Server {} is not a supported corresponding role. Supported: {:#?}", role.name, CorrespondingServer::VARIANTS), - ) - .into_response()); - }; - let role_id_snow = role.value.parse::().map_err(Error::new)?; - - let role_exists = { - let guild = state.cache_http.cache.guild(corr_server.get_id()); - - if let Some(guild) = guild { - guild.roles.get(&role_id_snow).is_some() - } else { - false - } - }; - - if !role_exists { - return Ok(( - StatusCode::BAD_REQUEST, - format!( - "Role {} does not exist on the server {}", - role_id_snow, - corr_server.get_id() - ), - ) - .into_response()); - } - } - - // Create the position - sqlx::query!( - "INSERT INTO staff_positions (name, perms, corresponding_roles, icon, role_id, index) VALUES ($1, $2, $3, $4, $5, $6)", - name, - &perms, - serde_json::to_value(corresponding_roles).map_err(Error::new)?, - icon, - role_id, - index, - ) - .execute(&mut *tx) - .await - .map_err(|e| format!("Error while updating position {}", e)) - .map_err(Error::new)?; - - tx.commit().await.map_err(Error::new)?; - - Ok((StatusCode::NO_CONTENT, "").into_response()) - } - StaffPositionAction::EditPosition { - id, - name, - role_id, - perms, - corresponding_roles, - icon, - } => { - let uuid = sqlx::types::uuid::Uuid::parse_str(&id).map_err(Error::new)?; - - // Get permissions - let sm = super::auth::get_staff_member( - &state.pool, - &state.cache_http, - &auth_data.user_id, - ) - .await - .map_err(Error::new)?; - - if !perms::has_perm(&sm.resolved_perms, &"staff_positions.edit".into()) { - return Ok(( - StatusCode::FORBIDDEN, - "You do not have permission to edit staff positions [staff_positions.edit]".to_string(), - ) - .into_response()); - } - - // Get the lowest index permission of the member - let mut sm_lowest_index = i32::MAX; - - for perm in &sm.positions { - if perm.index < sm_lowest_index { - sm_lowest_index = perm.index; - } - } - - let mut tx = state.pool.begin().await.map_err(Error::new)?; - - // Get the index and current permissions of the position - let index = sqlx::query!("SELECT perms, index, role_id FROM staff_positions WHERE id = $1 FOR UPDATE", uuid) - .fetch_one(&mut *tx) - .await - .map_err(|e| format!("Error while getting position {}", e)) - .map_err(Error::new)?; - - // If the index is lower than the lowest index of the member, then error - if index.index <= sm_lowest_index { - return Ok(( - StatusCode::FORBIDDEN, - "Index is lower than the lowest index of the member".to_string(), - ) - .into_response()); - } - - // Check perms - if let Err(e) = perms::check_patch_changes( - &sm.resolved_perms, - &index - .perms - .iter() - .map(|x| Permission::from_string(x)) - .collect::>(), - &perms - .iter() - .map(|x| Permission::from_string(x)) - .collect::>(), - ) { - return Ok(( - StatusCode::FORBIDDEN, - format!( - "You do not have permission to edit the following perms: {}", - e - ), - ) - .into_response()); - } - - // Ensure role id exists on the staff server - let role_id_snow = role_id.parse::().map_err(Error::new)?; - let role_exists = { - let guild = state - .cache_http - .cache - .guild(crate::config::CONFIG.servers.staff); - - if let Some(guild) = guild { - guild.roles.get(&role_id_snow).is_some() - } else { - false - } - }; - - if !role_exists { - return Ok(( - StatusCode::BAD_REQUEST, - "Role does not exist on the staff server".to_string(), - ) - .into_response()); - } - - // Ensure all corresponding_roles exist on the named server if - for role in corresponding_roles.iter() { - let Ok(corr_server) = CorrespondingServer::from_str(&role.name) else { - return Ok(( - StatusCode::BAD_REQUEST, - format!("Server {} is not a supported corresponding role. Supported: {:#?}", role.name, CorrespondingServer::VARIANTS), - ) - .into_response()); - }; - let role_id_snow = role.value.parse::().map_err(Error::new)?; - - let role_exists = { - let guild = state.cache_http.cache.guild(corr_server.get_id()); - - if let Some(guild) = guild { - guild.roles.get(&role_id_snow).is_some() - } else { - false - } - }; - - if !role_exists { - return Ok(( - StatusCode::BAD_REQUEST, - format!( - "Role {} does not exist on the server {}", - role_id_snow, - corr_server.get_id() - ), - ) - .into_response()); - } - } - - // Update the position - sqlx::query!( - "UPDATE staff_positions SET name = $1, perms = $2, corresponding_roles = $3, role_id = $4, icon = $5 WHERE id = $6", - name, - &perms, - serde_json::to_value(corresponding_roles).map_err(Error::new)?, - role_id, - icon, - uuid - ) - .execute(&mut *tx) - .await - .map_err(|e| format!("Error while updating position {}", e)) - .map_err(Error::new)?; - - tx.commit().await.map_err(Error::new)?; - - Ok((StatusCode::NO_CONTENT, "").into_response()) - } - StaffPositionAction::DeletePosition { id } => { - let uuid = sqlx::types::uuid::Uuid::parse_str(&id).map_err(Error::new)?; - - // Get permissions - let sm = super::auth::get_staff_member( - &state.pool, - &state.cache_http, - &auth_data.user_id, - ) - .await - .map_err(Error::new)?; - - if !perms::has_perm(&sm.resolved_perms, &"staff_positions.delete".into()) { - return Ok(( - StatusCode::FORBIDDEN, - "You do not have permission to delete staff positions [staff_positions.delete]".to_string(), - ) - .into_response()); - } - - // Get the lowest index permission of the member - let mut sm_lowest_index = i32::MAX; - - for perm in &sm.positions { - if perm.index < sm_lowest_index { - sm_lowest_index = perm.index; - } - } - - let mut tx = state.pool.begin().await.map_err(Error::new)?; - - // Get the index and current permissions of the position - let index = sqlx::query!("SELECT perms, index, role_id FROM staff_positions WHERE id = $1 FOR UPDATE", uuid) - .fetch_one(&mut *tx) - .await - .map_err(|e| format!("Error while getting position {}", e)) - .map_err(Error::new)?; - - // If the index is lower than the lowest index of the member, then error - if index.index <= sm_lowest_index { - return Ok(( - StatusCode::FORBIDDEN, - "Index is lower than the lowest index of the member".to_string(), - ) - .into_response()); - } + } + PanelQuery::GetRpcLogEntries { login_token } => { + let auth_data = super::auth::check_auth(&state.pool, &login_token) + .await + .map_err(Error::new)?; - // Check perms - if let Err(e) = perms::check_patch_changes( - &sm.resolved_perms, - &index - .perms - .iter() - .map(|x| Permission::from_string(x)) - .collect::>(), - &Vec::new(), - ) { - return Ok(( - StatusCode::FORBIDDEN, - format!("You do not have permission to edit the following perms [neeed to delete position]: {}", e), - ) - .into_response()); - } + let user_perms = get_user_perms(&state.pool, &auth_data.user_id) + .await + .map_err(Error::new)? + .resolve(); - // Delete the position - sqlx::query!("DELETE FROM staff_positions WHERE id = $1", uuid) - .execute(&mut *tx) - .await - .map_err(|e| format!("Error while deleting position {}", e)) - .map_err(Error::new)?; + if !perms::has_perm(&user_perms, &"rpc_logs.view".into()) { + return Ok(( + StatusCode::FORBIDDEN, + "You do not have permission to view rpc logs [rpc_logs.view]".to_string(), + ) + .into_response()); + } - // Shift back indexes one lower - sqlx::query!( - "UPDATE staff_positions SET index = index - 1 WHERE index > $1", - index.index - ) - .execute(&mut *tx) - .await - .map_err(|e| format!("Error while shifting indexes {}", e)) - .map_err(Error::new)?; + let entries = sqlx::query!( + "SELECT id, user_id, method, data, state, created_at FROM rpc_logs ORDER BY created_at DESC" + ) + .fetch_all(&state.pool) + .await + .map_err(Error::new)?; - tx.commit().await.map_err(Error::new)?; + let mut rpc_log = vec![]; - Ok((StatusCode::NO_CONTENT, "").into_response()) - } + for entry in entries { + rpc_log.push(RPCLogEntry { + id: entry.id.to_string(), + user_id: entry.user_id, + method: entry.method, + data: entry.data, + state: entry.state, + created_at: entry.created_at, + }); } + + Ok((StatusCode::OK, Json(rpc_log)).into_response()) } - PanelQuery::UpdateStaffMembers { + PanelQuery::SearchEntitys { + login_token, + target_type, + query, + } => { + super::actions::searchentitys::search_entitys(&state, login_token, target_type, query) + .await + } + PanelQuery::UpdatePartners { + login_token, + action, + } => super::actions::updatepartners::update_partners(&state, login_token, action).await, + PanelQuery::UpdateBlog { login_token, action, } => { @@ -2592,142 +340,178 @@ async fn query( .await .map_err(Error::new)?; - match action { - StaffMemberAction::ListMembers => { - let ids = sqlx::query!("SELECT user_id FROM staff_members") - .fetch_all(&state.pool) - .await - .map_err(|e| format!("Error while getting staff members {}", e)) - .map_err(Error::new)?; + let user_perms = get_user_perms(&state.pool, &auth_data.user_id) + .await + .map_err(Error::new)? + .resolve(); - let mut members = Vec::new(); + // TODO: Make this not require a wasteful query + let ad = super::auth::check_auth(&state.pool, &login_token) + .await + .map_err(Error::new)?; - for id in ids { - let member = super::auth::get_staff_member( - &state.pool, - &state.cache_http, - &id.user_id, - ) - .await - .map_err(Error::new)?; + match action { + BlogAction::ListEntries => { + let rows = sqlx::query!( + "SELECT itag, slug, title, description, user_id, content, created_at, draft, tags FROM blogs ORDER BY created_at DESC" + ) + .fetch_all(&state.pool) + .await + .map_err(Error::new)?; + + let mut entries = Vec::new(); - members.push(member); + for row in rows { + entries.push(BlogPost { + itag: row.itag.hyphenated().to_string(), + slug: row.slug, + title: row.title, + description: row.description, + user_id: row.user_id, + tags: row.tags, + content: row.content, + created_at: row.created_at, + draft: row.draft, + }); } - Ok((StatusCode::OK, Json(members)).into_response()) + Ok((StatusCode::OK, Json(entries)).into_response()) } - StaffMemberAction::EditMember { - user_id, - perm_overrides, - no_autosync, - unaccounted, + BlogAction::CreateEntry { + slug, + title, + description, + content, + tags, } => { - // Get permissions - let sm = super::auth::get_staff_member( - &state.pool, - &state.cache_http, - &auth_data.user_id, + if !perms::has_perm(&user_perms, &"blog.create_entry".into()) { + return Ok(( + StatusCode::FORBIDDEN, + "You do not have permission to create blog entries [blog.create_entry]" + .to_string(), + ) + .into_response()); + } + + // Insert entry + sqlx::query!( + "INSERT INTO blogs (slug, title, description, content, tags, user_id) VALUES ($1, $2, $3, $4, $5, $6)", + slug, + title, + description, + content, + &tags, + &ad.user_id, ) + .execute(&state.pool) .await .map_err(Error::new)?; - // Get permissions of target - let sm_target = - super::auth::get_staff_member(&state.pool, &state.cache_http, &user_id) - .await - .map_err(Error::new)?; - - if !perms::has_perm(&sm.resolved_perms, &"staff_members.edit".into()) { + Ok((StatusCode::NO_CONTENT, "").into_response()) + } + BlogAction::UpdateEntry { + itag, + slug, + title, + description, + content, + tags, + draft, + } => { + if !perms::has_perm(&user_perms, &"blog.update_entry".into()) { return Ok(( StatusCode::FORBIDDEN, - "You do not have permission to edit staff members [staff_members.edit]" + "You do not have permission to update blog entries [blog.update_entry]" .to_string(), ) .into_response()); } - // Get the lowest index permission of the member - let mut sm_lowest_index = i32::MAX; + let uuid = sqlx::types::uuid::Uuid::parse_str(&itag).map_err(Error::new)?; - for perm in &sm.positions { - if perm.index < sm_lowest_index { - sm_lowest_index = perm.index; - } + // Check if entry already exists with same vesion + if sqlx::query!("SELECT COUNT(*) FROM blogs WHERE itag = $1", uuid) + .fetch_one(&state.pool) + .await + .map_err(Error::new)? + .count + .unwrap_or(0) + == 0 + { + return Ok( + (StatusCode::BAD_REQUEST, "Entry does not exist".to_string()) + .into_response(), + ); } - // Get the lowest index permission of the target - let mut sm_target_lowest_index = i32::MAX; - - for perm in &sm_target.positions { - if perm.index < sm_target_lowest_index { - sm_target_lowest_index = perm.index; - } - } + // Update entry + sqlx::query!( + "UPDATE blogs SET slug = $2, title = $3, description = $4, content = $5, tags = $6, draft = $7 WHERE itag = $1", + uuid, + slug, + title, + description, + content, + &tags, + draft + ) + .execute(&state.pool) + .await + .map_err(Error::new)?; - // If the target has a lower index than the member, then error - if sm_target_lowest_index < sm_lowest_index { + Ok((StatusCode::NO_CONTENT, "").into_response()) + } + BlogAction::DeleteEntry { itag } => { + if !perms::has_perm(&user_perms, &"blog.delete_entry".into()) { return Ok(( StatusCode::FORBIDDEN, - "Target has a lower index than the member".to_string(), + "You do not have permission to delete blog entries [blog.delete_entry]" + .to_string(), ) .into_response()); } - let perm_overrides = perm_overrides - .iter() - .map(|x| Permission::from_string(x)) - .collect::>(); - - // Check perms with resolved perms following addition of overrides - let new_resolved_perms = perms::StaffPermissions { - perm_overrides: perm_overrides.clone(), - ..sm_target.staff_permission - } - .resolve(); - - if let Err(e) = perms::check_patch_changes( - &sm.resolved_perms, - &sm_target.resolved_perms, - &new_resolved_perms, - ) { + // Check if entry already exists with same vesion + let uuid = sqlx::types::uuid::Uuid::parse_str(&itag).map_err(Error::new)?; + if sqlx::query!("SELECT COUNT(*) FROM blogs WHERE itag = $1", uuid) + .fetch_one(&state.pool) + .await + .map_err(Error::new)? + .count + .unwrap_or(0) + == 0 + { return Ok(( - StatusCode::FORBIDDEN, - format!( - "You do not have permission to edit the following perms: {}", - e - ), + StatusCode::BAD_REQUEST, + "Entry with same id does not already exist".to_string(), ) .into_response()); } - // Then update - let mut tx = state.pool.begin().await.map_err(Error::new)?; - - // Lock the member for update - sqlx::query!("SELECT perm_overrides, no_autosync, unaccounted FROM staff_members WHERE user_id = $1 FOR UPDATE", user_id) - .fetch_one(&mut *tx) - .await - .map_err(|e| format!("Error while getting member {}", e)) - .map_err(Error::new)?; - - // Update the member - sqlx::query!("UPDATE staff_members SET perm_overrides = $1, no_autosync = $2, unaccounted = $3 WHERE user_id = $4", - &perm_overrides.iter().map(|x| x.to_string()).collect::>(), - no_autosync, - unaccounted, - user_id - ) - .execute(&mut *tx) - .await - .map_err(|e| format!("Error while updating member {}", e)) - .map_err(Error::new)?; - - tx.commit().await.map_err(Error::new)?; + // Delete entry + sqlx::query!("DELETE FROM blogs WHERE itag = $1", uuid) + .execute(&state.pool) + .await + .map_err(Error::new)?; Ok((StatusCode::NO_CONTENT, "").into_response()) } } } + PanelQuery::UpdateStaffPositions { + login_token, + action, + } => { + super::actions::updatestaffposition::update_staff_position(&state, login_token, action) + .await + } + PanelQuery::UpdateStaffMembers { + login_token, + action, + } => { + super::actions::updatestaffmembers::update_staff_members(&state, login_token, action) + .await + } PanelQuery::UpdateStaffDisciplinaryType { login_token, action, @@ -4022,53 +1806,5 @@ async fn query( } } } - PanelQuery::PopplioStaff { - login_token, - path, - method, - body, - } => { - let auth_data = super::auth::check_auth(&state.pool, &login_token) - .await - .map_err(Error::new)?; - - let client = reqwest::Client::new(); - - let Ok(method) = reqwest::Method::from_bytes(&method.into_bytes()) else { - return Ok((StatusCode::BAD_REQUEST, "Invalid method".to_string()).into_response()); - }; - - if !path.starts_with('/') { - return Ok(( - StatusCode::BAD_REQUEST, - "Path must start with /".to_string(), - ) - .into_response()); - } - - let query = sqlx::query!( - "SELECT popplio_token FROM staffpanel__authchain WHERE token = $1", - login_token - ) - .fetch_one(&state.pool) - .await - .map_err(Error::new)?; - - let res = client - .request(method, crate::config::CONFIG.popplio_url.clone() + &path) - .header("User-Agent", "arcadia") - .header("X-Forwarded-For", &path) - .header("X-Staff-Auth-Token", &query.popplio_token) - .header("X-User-ID", &auth_data.user_id) - .body(body) - .send() - .await - .map_err(Error::new)?; - - let status = res.status(); - let resp = res.text().await.map_err(Error::new)?; - let http_status = StatusCode::from_u16(status.as_u16()).unwrap(); - Ok((http_status, resp).into_response()) - } } } diff --git a/src/panelapi/types/cdn.rs b/src/panelapi/types/cdn.rs deleted file mode 100644 index 31a070d7..00000000 --- a/src/panelapi/types/cdn.rs +++ /dev/null @@ -1,80 +0,0 @@ -use serde::{Deserialize, Serialize}; -use strum_macros::{Display, EnumString, EnumVariantNames}; -use ts_rs::TS; -use utoipa::ToSchema; - -#[derive( - Serialize, - Deserialize, - ToSchema, - TS, - EnumString, - EnumVariantNames, - Display, - Clone, - PartialEq, - Default, -)] -#[ts(export, export_to = ".generated/CdnAssetAction.ts")] -pub enum CdnAssetAction { - /// List entries in path - /// - /// Using this ignores the `name` field - #[default] - ListPath, - /// Read an asset - ReadFile, - /// Creates a new folder - CreateFolder, - /// Creates an asset - /// - /// The file itself must not already exist - AddFile { - /// Allow overwrite of existing file - overwrite: bool, - /// Base 64 encoded file contents uploaded as multiple chunks with an ID associated with each chunk - /// - /// Note that uploading chunks needs `cdn.upload_chunk` permission. As it is not possible to upload without - /// using chunks, disabling `cdn.upload_chunk` will disable uploading files without impacting other operations - chunks: Vec, - /// SHA512 hash of the file - sha512: String, - }, - /// Copies an asset already on the server to a new location - CopyFile { - /// Allow overwrite of existing file - overwrite: bool, - /// Delete the original file - delete_original: bool, - /// Path to copy to - copy_to: String, - }, - /// Delete asset or folder - Delete, - /// Make github commit to persist files to github - PersistGit { - /// Commit message - message: String, - /// Current directory push - /// - /// Using this option will only add and push files in the current directory - current_dir: bool, - }, -} - -#[derive(Serialize, Deserialize, TS, ToSchema, Clone)] -#[ts(export, export_to = ".generated/CdnAssetItem.ts")] -pub struct CdnAssetItem { - /// Name of the asset - pub name: String, - /// Path of the asset - pub path: String, - /// Size of the asset - pub size: u64, - /// Last modified time of the asset as unix epoch - pub last_modified: u64, - /// Whether the asset is a directory - pub is_dir: bool, - /// Permissions of the asset - pub permissions: u32, -} diff --git a/src/panelapi/types/changelogs.rs b/src/panelapi/types/changelogs.rs deleted file mode 100644 index 15d4aa9e..00000000 --- a/src/panelapi/types/changelogs.rs +++ /dev/null @@ -1,81 +0,0 @@ -use serde::{Deserialize, Serialize}; -use strum_macros::{Display, EnumString, EnumVariantNames}; -use ts_rs::TS; -use utoipa::ToSchema; - -#[derive( - Serialize, - Deserialize, - ToSchema, - TS, - EnumString, - EnumVariantNames, - Display, - Clone, - PartialEq, - Default, -)] -#[ts(export, export_to = ".generated/ChangelogAction.ts")] -pub enum ChangelogAction { - /// List changelog entries - /// - /// Note that all staff members can list the changelog - #[default] - ListEntries, - - /// Create a new changelog entry - CreateEntry { - /// Version for the changelog entry to add - version: String, - /// Extra description for the version, if applicable - extra_description: String, - /// Whether or not this is a prerelease - prerelease: bool, - /// Added features for the version - added: Vec, - /// Updated features for the version - updated: Vec, - /// Removed features for the version - removed: Vec, - }, - - /// Update a changelog entry - UpdateEntry { - /// Version for the changelog entry to update - version: String, - /// Extra description for the version, if applicable - extra_description: String, - /// Github HTML for the version, if applicable - github_html: Option, - /// Whether or not this is a prerelease - prerelease: bool, - /// Added features for the version - added: Vec, - /// Updated features for the version - updated: Vec, - /// Removed features for the version - removed: Vec, - /// Whether or not to publish the version - published: bool, - }, - - /// Delete a changelog entry - DeleteEntry { - /// Version for the changelog entry to delete - version: String, - }, -} - -#[derive(Serialize, Deserialize, TS, ToSchema, Clone)] -#[ts(export, export_to = ".generated/ChangelogEntry.ts")] -pub struct ChangelogEntry { - pub version: String, - pub added: Vec, - pub updated: Vec, - pub removed: Vec, - pub github_html: Option, - pub created_at: chrono::DateTime, - pub extra_description: String, - pub prerelease: bool, - pub published: bool, -} diff --git a/src/panelapi/types/mod.rs b/src/panelapi/types/mod.rs index f33e2fa0..92d7d1e8 100644 --- a/src/panelapi/types/mod.rs +++ b/src/panelapi/types/mod.rs @@ -2,8 +2,6 @@ pub mod analytics; pub mod auth; pub mod blog; pub mod bot_whitelist; -pub mod cdn; -pub mod changelogs; pub mod entity; pub mod partners; pub mod rpc; diff --git a/src/tasks/__toberewritten/README.md b/src/tasks/__toberewritten/README.md deleted file mode 100644 index 4fef15f5..00000000 --- a/src/tasks/__toberewritten/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# To be rewritten - -Files under this folder are to be rewritten as they do not work \ No newline at end of file diff --git a/src/tasks/__toberewritten/uptime.rs b/src/tasks/__toberewritten/uptime.rs deleted file mode 100644 index 6e679080..00000000 --- a/src/tasks/__toberewritten/uptime.rs +++ /dev/null @@ -1,116 +0,0 @@ -use std::num::NonZeroU64; - -use log::info; -use poise::serenity_prelude::{ChannelId, CreateEmbed, CreateEmbedFooter, CreateMessage, GuildId}; - -pub async fn uptime_checker( - pool: &sqlx::PgPool, - cache_http: &botox::cache::CacheHttpImpl, -) -> Result<(), crate::Error> { - let subject_rows = sqlx::query!( - "SELECT bot_id, uptime, total_uptime FROM bots WHERE (type = 'approved' OR type = 'certified') AND (NOW() - uptime_last_checked > interval '30 minutes')" - ) - .fetch_all(pool) - .await?; - - let presences = { - if let Some(guild) = cache_http - .cache - .guild(GuildId(crate::config::CONFIG.servers.main)) - { - Some(guild.presences.clone()) - } else { - None - } - } - .ok_or("Could not find main server")?; - - for row in subject_rows { - // Find bot in cache - let bot_snow = match row.bot_id.parse::() { - Ok(snow) => snow, - Err(_) => { - log::warn!("Invalid bot id: {}", row.bot_id); - continue; - } - }; - - // Find user in precense cache - match cache_http.cache.member_field( - GuildId(crate::config::CONFIG.servers.main), - bot_snow, - |m| m.user.id, - ) { - Some(precense) => { - let uptime = match presences.get(&precense) { - Some(precense) => { - precense.status != poise::serenity_prelude::OnlineStatus::Offline - } - None => false, - }; - - if uptime { - sqlx::query!( - "UPDATE bots SET uptime = uptime + 1, total_uptime = total_uptime + 1 WHERE bot_id = $1", - row.bot_id - ) - .execute(pool) - .await?; - } else { - log::warn!("Bot {} is offline", row.bot_id); - sqlx::query!( - "UPDATE bots SET total_uptime = total_uptime + 1 WHERE bot_id = $1", - row.bot_id - ) - .execute(pool) - .await?; - - let uptime_rate = ((row.uptime + 1) / (row.total_uptime + 1)) * 100; - - info!("Uptime rate: {} for bot {}", uptime_rate, row.bot_id); - - if (uptime_rate > 0 && uptime_rate < 50) - && (row.uptime > 0 && row.total_uptime > 25) - { - // Send message to mod logs - let msg = CreateMessage::default().embed( - CreateEmbed::default() - .title("Bot Uptime Warning!") - .url(format!( - "{}/bots/{}", - crate::config::CONFIG.frontend_url, - row.bot_id - )) - .description(format!( - "<@!{}> a lower uptime than 50% with over 25 uptime checks", - row.bot_id - )) - .field("Bot", "<@!".to_string() + &row.bot_id + ">", true) - .footer(CreateEmbedFooter::new( - "Please check this bot and ensure its actually alive!", - )) - .color(0x00ff00), - ); - - ChannelId(crate::config::CONFIG.channels.uptime) - .send_message(&cache_http, msg) - .await?; - } - - sqlx::query!( - "UPDATE bots SET uptime_last_checked = NOW() WHERE bot_id = $1", - row.bot_id - ) - .execute(pool) - .await?; - } - } - None => { - log::warn!("Could not find bot {} in cache", row.bot_id); - continue; - } - } - } - - Ok(()) -} From 11dc4981fa1dc4b741fc6bd6152965e430a77004 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 30 Dec 2024 16:04:23 +0000 Subject: [PATCH 2/4] add updateshopholds --- Cargo.lock | 4 +- Cargo.toml | 27 +- src/panelapi/actions/mod.rs | 2 + src/panelapi/actions/updateshopholds.rs | 235 +++++++++++++++ src/panelapi/actions/updatevotecredittiers.rs | 285 ++++++++++++++++++ src/panelapi/panel_query.rs | 9 +- src/panelapi/server.rs | 275 +---------------- src/panelapi/types/shop_items.rs | 72 +++++ 8 files changed, 633 insertions(+), 276 deletions(-) create mode 100644 src/panelapi/actions/updateshopholds.rs create mode 100644 src/panelapi/actions/updatevotecredittiers.rs diff --git a/Cargo.lock b/Cargo.lock index aa31c6de..5898d196 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "Inflector" @@ -331,6 +331,7 @@ dependencies = [ "tower-http", "ts-rs", "utoipa", + "uuid", "vergen", ] @@ -3561,6 +3562,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" dependencies = [ "getrandom", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c3ee9a57..4ef3451c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,8 @@ version = "1.0.1" edition = "2021" [profile.release] -strip = true # Automatically strip symbols from the binary. -panic = "abort" # Abort on panic. This is what we want for a bot. +strip = true # Automatically strip symbols from the binary. +panic = "abort" # Abort on panic. This is what we want for a bot. # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -16,9 +16,20 @@ serde = "1.0" log = "0.4" env_logger = "0.9" serde_json = "1.0" -sqlx = { version = "0.8", features = [ "runtime-tokio-rustls", "postgres", "chrono", "uuid", "bigdecimal", "json" ] } -reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls", "rustls-tls-native-roots"] } -chrono = { version = "0.4", features = ["serde"]} +sqlx = { version = "0.8", features = [ + "runtime-tokio-rustls", + "postgres", + "chrono", + "uuid", + "bigdecimal", + "json", +] } +reqwest = { version = "0.11", default-features = false, features = [ + "json", + "rustls-tls", + "rustls-tls-native-roots", +] } +chrono = { version = "0.4", features = ["serde"] } futures-util = "0.3" ring = "0.16" data-encoding = "2.3" @@ -31,11 +42,15 @@ serde_yaml = "0.9" once_cell = "1.17" strum = "0.24" strum_macros = "0.24" -moka = { version = "0.11", default-features = true, features = ["future", "logging"] } +moka = { version = "0.11", default-features = true, features = [ + "future", + "logging", +] } thotp = "0.1.11" tokio-util = "0.7.8" sha2 = "0.10.7" num-traits = "0.2.14" +uuid = { version = "1", features = ["serde"] } [dependencies.tokio] version = "1" diff --git a/src/panelapi/actions/mod.rs b/src/panelapi/actions/mod.rs index 1dafef8a..5ca859f9 100644 --- a/src/panelapi/actions/mod.rs +++ b/src/panelapi/actions/mod.rs @@ -4,5 +4,7 @@ pub mod getuser; pub mod hello; pub mod searchentitys; pub mod updatepartners; +pub mod updateshopholds; pub mod updatestaffmembers; pub mod updatestaffposition; +pub mod updatevotecredittiers; diff --git a/src/panelapi/actions/updateshopholds.rs b/src/panelapi/actions/updateshopholds.rs new file mode 100644 index 00000000..8fd0e5c3 --- /dev/null +++ b/src/panelapi/actions/updateshopholds.rs @@ -0,0 +1,235 @@ +use crate::impls::utils::get_user_perms; +use crate::panelapi::auth::check_auth; +use crate::panelapi::core::{AppState, Error}; +use crate::panelapi::types::shop_items::{ShopHold, ShopHoldAction}; +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + Json, +}; +use kittycat::perms; + +pub async fn update_shop_holds( + state: &AppState, + login_token: String, + action: ShopHoldAction, +) -> Result { + let auth_data = check_auth(&state.pool, &login_token) + .await + .map_err(Error::new)?; + + let user_perms = get_user_perms(&state.pool, &auth_data.user_id) + .await + .map_err(Error::new)? + .resolve(); + + match action { + ShopHoldAction::List => { + let rows = sqlx::query!( + "SELECT id, target_id, target_type, item, created_at, duration FROM shop_holds ORDER BY created_at ASC" + ) + .fetch_all(&state.pool) + .await + .map_err(Error::new)?; + + let mut entries = Vec::new(); + + for row in rows { + entries.push(ShopHold { + id: row.id, + target_id: row.target_id, + target_type: row.target_type, + item: row.item, + created_at: row.created_at, + duration: row.duration.map(|d| { + let months = d.months as i64; + let days = d.days as i64; + let microseconds = d.microseconds; + let micros = months * 30 * 24 * 60 * 60 * 1_000_000 + + days * 24 * 60 * 60 * 1_000_000 + + microseconds; + + micros / 1_000_000 + }), + }); + } + + Ok((StatusCode::OK, Json(entries)).into_response()) + } + ShopHoldAction::Create { + target_id, + target_type, + item, + duration, + } => { + if !perms::has_perm(&user_perms, &"shop_holds.create".into()) { + return Ok(( + StatusCode::FORBIDDEN, + "You do not have permission to create shop holds [shop_holds.create]" + .to_string(), + ) + .into_response()); + } + + let item_exists = sqlx::query!("SELECT COUNT(*) FROM shop_items WHERE id = $1", item,) + .fetch_one(&state.pool) + .await + .map_err(Error::new)? + .count + .unwrap_or(0) + > 0; + + if !item_exists { + return Ok( + (StatusCode::BAD_REQUEST, "Item does not exist".to_string()).into_response() + ); + } + + if target_type != "bot" && target_type != "server" { + return Ok(( + StatusCode::BAD_REQUEST, + "Target type must be either 'bot' or 'server'".to_string(), + ) + .into_response()); + } + + // Insert entry + let duration = duration.map(|d| { + // Make PgInterval from duration. Duration is in seconds + let d = d as i64; + sqlx::postgres::types::PgInterval { + microseconds: d * 1_000_000, + ..Default::default() + } + }); + + let mut tx = state.pool.begin().await.map_err(Error::new)?; + sqlx::query!( + "INSERT INTO shop_holds (target_id, target_type, item, duration) VALUES ($1, $2, $3, $4)", + target_id, + target_type, + item, + duration, + ) + .execute(&mut *tx) + .await + .map_err(Error::new)?; + + tx.commit().await.map_err(Error::new)?; + + Ok((StatusCode::NO_CONTENT, "").into_response()) + } + ShopHoldAction::Edit { + id, + target_id, + target_type, + item, + duration, + } => { + if !perms::has_perm(&user_perms, &"shop_holds.update".into()) { + return Ok(( + StatusCode::FORBIDDEN, + "You do not have permission to update shop holds [shop_holds.update]" + .to_string(), + ) + .into_response()); + } + + // Check if entry already exists with same id + if sqlx::query!("SELECT COUNT(*) FROM shop_holds WHERE id = $1", id) + .fetch_one(&state.pool) + .await + .map_err(Error::new)? + .count + .unwrap_or(0) + == 0 + { + return Ok(( + StatusCode::BAD_REQUEST, + "Entry with same id does not already exist".to_string(), + ) + .into_response()); + } + + let item_exists = sqlx::query!("SELECT COUNT(*) FROM shop_items WHERE id = $1", item,) + .fetch_one(&state.pool) + .await + .map_err(Error::new)? + .count + .unwrap_or(0) + > 0; + + if !item_exists { + return Ok( + (StatusCode::BAD_REQUEST, "Item does not exist".to_string()).into_response() + ); + } + + if target_type != "bot" && target_type != "server" { + return Ok(( + StatusCode::BAD_REQUEST, + "Target type must be either 'bot' or 'server'".to_string(), + ) + .into_response()); + } + + // Update entry + let duration = duration.map(|d| { + // Make PgInterval from duration. Duration is in seconds + let d = d as i64; + sqlx::postgres::types::PgInterval { + microseconds: d * 1_000_000, + ..Default::default() + } + }); + + sqlx::query!( + "UPDATE shop_holds SET target_id = $1, target_type = $2, item = $3, duration = $4 WHERE id = $5", + target_id, + target_type, + item, + duration, + id, + ) + .execute(&state.pool) + .await + .map_err(Error::new)?; + + Ok((StatusCode::NO_CONTENT, "").into_response()) + } + ShopHoldAction::Delete { id } => { + if !perms::has_perm(&user_perms, &"shop_holds.delete".into()) { + return Ok(( + StatusCode::FORBIDDEN, + "You do not have permission to delete shop holds [shop_holds.delete]" + .to_string(), + ) + .into_response()); + } + + // Check if entry already exists + if sqlx::query!("SELECT COUNT(*) FROM shop_holds WHERE id = $1", id) + .fetch_one(&state.pool) + .await + .map_err(Error::new)? + .count + .unwrap_or(0) + == 0 + { + return Ok(( + StatusCode::BAD_REQUEST, + "Entry with same id does not already exist".to_string(), + ) + .into_response()); + } + + // Delete entry + sqlx::query!("DELETE FROM shop_holds WHERE id = $1", id) + .execute(&state.pool) + .await + .map_err(Error::new)?; + + Ok((StatusCode::NO_CONTENT, "").into_response()) + } + } +} diff --git a/src/panelapi/actions/updatevotecredittiers.rs b/src/panelapi/actions/updatevotecredittiers.rs new file mode 100644 index 00000000..b8814a3e --- /dev/null +++ b/src/panelapi/actions/updatevotecredittiers.rs @@ -0,0 +1,285 @@ +use crate::impls::utils::get_user_perms; +use crate::panelapi::auth::check_auth; +use crate::panelapi::core::{AppState, Error}; +use crate::panelapi::types::vote_credit_tiers::{VoteCreditTier, VoteCreditTierAction}; +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + Json, +}; +use kittycat::perms; + +pub async fn update_vote_credit_tiers( + state: &AppState, + login_token: String, + action: VoteCreditTierAction, +) -> Result { + let auth_data = check_auth(&state.pool, &login_token) + .await + .map_err(Error::new)?; + + let user_perms = get_user_perms(&state.pool, &auth_data.user_id) + .await + .map_err(Error::new)? + .resolve(); + + match action { + VoteCreditTierAction::ListTiers => { + let rows = sqlx::query!( + "SELECT id, target_type, position, cents, votes, created_at FROM vote_credit_tiers ORDER BY position ASC" + ) + .fetch_all(&state.pool) + .await + .map_err(Error::new)?; + + let mut entries = Vec::new(); + + for row in rows { + entries.push(VoteCreditTier { + id: row.id, + target_type: row.target_type, + position: row.position, + cents: row.cents, + votes: row.votes, + created_at: row.created_at, + }); + } + + Ok((StatusCode::OK, Json(entries)).into_response()) + } + VoteCreditTierAction::CreateTier { + id, + position, + target_type, + cents, + votes, + } => { + if !perms::has_perm(&user_perms, &"vote_credit_tiers.create".into()) { + return Ok(( + StatusCode::FORBIDDEN, + "You do not have permission to create vote credit tiers [vote_credit_tiers.create]".to_string(), + ) + .into_response()); + } + + if cents < 0.0 { + return Ok(( + StatusCode::BAD_REQUEST, + "Cents cannot be lower than 0".to_string(), + ) + .into_response()); + } + + if votes < 0 { + return Ok(( + StatusCode::BAD_REQUEST, + "Votes cannot be lower than 0".to_string(), + ) + .into_response()); + } + + if target_type != "bot" && target_type != "server" { + return Ok(( + StatusCode::BAD_REQUEST, + "Target type must be either 'bot' or 'server'".to_string(), + ) + .into_response()); + } + + // Insert entry + let mut tx = state.pool.begin().await.map_err(Error::new)?; + sqlx::query!( + "INSERT INTO vote_credit_tiers (id, target_type, position, cents, votes) VALUES ($1, $2, $3, $4, $5)", + id, + target_type, + position, + cents, + votes, + ) + .execute(&mut *tx) + .await + .map_err(Error::new)?; + + // Now keep shifting positions until they are all unique + let mut index_a = position; + + loop { + let rows = sqlx::query!( + "SELECT id, position FROM vote_credit_tiers WHERE position = $1 AND id != $2", + index_a, + id, + ) + .fetch_all(&mut *tx) + .await + .map_err(Error::new)?; + + if rows.is_empty() { + break; + } + + let mut index_b = index_a + 1; + + for row in rows { + sqlx::query!( + "UPDATE vote_credit_tiers SET position = $1 WHERE id = $2", + index_b, + row.id, + ) + .execute(&mut *tx) + .await + .map_err(Error::new)?; + + index_b += 1; + } + + index_a = index_b; + } + + tx.commit().await.map_err(Error::new)?; + + Ok((StatusCode::NO_CONTENT, "").into_response()) + } + VoteCreditTierAction::EditTier { + id, + position, + target_type, + cents, + votes, + } => { + if !perms::has_perm(&user_perms, &"vote_credit_tiers.update".into()) { + return Ok(( + StatusCode::FORBIDDEN, + "You do not have permission to update vote credit tiers [vote_credit_tiers.update]".to_string(), + ) + .into_response()); + } + + // Check if entry already exists with same id + if sqlx::query!("SELECT COUNT(*) FROM vote_credit_tiers WHERE id = $1", id) + .fetch_one(&state.pool) + .await + .map_err(Error::new)? + .count + .unwrap_or(0) + == 0 + { + return Ok(( + StatusCode::BAD_REQUEST, + "Entry with same id does not already exist".to_string(), + ) + .into_response()); + } + + if cents < 0.0 { + return Ok(( + StatusCode::BAD_REQUEST, + "Cents cannot be lower than 0".to_string(), + ) + .into_response()); + } + + if votes < 0 { + return Ok(( + StatusCode::BAD_REQUEST, + "Votes cannot be lower than 0".to_string(), + ) + .into_response()); + } + + if target_type != "bot" && target_type != "server" { + return Ok(( + StatusCode::BAD_REQUEST, + "Target type must be either 'bot' or 'server'".to_string(), + ) + .into_response()); + } + + let mut tx = state.pool.begin().await.map_err(Error::new)?; + + // Update entry + sqlx::query!( + "UPDATE vote_credit_tiers SET position = $1, target_type = $2, cents = $3, votes = $4 WHERE id = $5", + position, + target_type, + cents, + votes, + id, + ) + .execute(&mut *tx) + .await + .map_err(Error::new)?; + + // Now keep shifting positions until they are all unique + let mut index_a = position; + + loop { + let rows = sqlx::query!( + "SELECT id, position FROM vote_credit_tiers WHERE position = $1 AND id != $2", + index_a, + id, + ) + .fetch_all(&mut *tx) + .await + .map_err(Error::new)?; + + if rows.is_empty() { + break; + } + + let mut index_b = index_a + 1; + + for row in rows { + sqlx::query!( + "UPDATE vote_credit_tiers SET position = $1 WHERE id = $2", + index_b, + row.id, + ) + .execute(&mut *tx) + .await + .map_err(Error::new)?; + + index_b += 1; + } + + index_a = index_b; + } + + tx.commit().await.map_err(Error::new)?; + + Ok((StatusCode::NO_CONTENT, "").into_response()) + } + VoteCreditTierAction::DeleteTier { id } => { + if !perms::has_perm(&user_perms, &"vote_credit_tiers.delete".into()) { + return Ok(( + StatusCode::FORBIDDEN, + "You do not have permission to delete vote credit tiers [vote_credit_tiers.delete]".to_string(), + ) + .into_response()); + } + + // Check if entry already exists with same vesion + if sqlx::query!("SELECT COUNT(*) FROM vote_credit_tiers WHERE id = $1", id) + .fetch_one(&state.pool) + .await + .map_err(Error::new)? + .count + .unwrap_or(0) + == 0 + { + return Ok(( + StatusCode::BAD_REQUEST, + "Entry with same id does not already exist".to_string(), + ) + .into_response()); + } + + // Delete entry + sqlx::query!("DELETE FROM vote_credit_tiers WHERE id = $1", id) + .execute(&state.pool) + .await + .map_err(Error::new)?; + + Ok((StatusCode::NO_CONTENT, "").into_response()) + } + } +} diff --git a/src/panelapi/panel_query.rs b/src/panelapi/panel_query.rs index 36d8fd39..1fa8c314 100644 --- a/src/panelapi/panel_query.rs +++ b/src/panelapi/panel_query.rs @@ -6,7 +6,7 @@ use crate::panelapi::types::{ blog::BlogAction, bot_whitelist::BotWhitelistAction, partners::PartnerAction, - shop_items::{ShopCouponAction, ShopItemAction, ShopItemBenefitAction}, + shop_items::{ShopCouponAction, ShopHoldAction, ShopItemAction, ShopItemBenefitAction}, staff_disciplinary::StaffDisciplinaryTypeAction, vote_credit_tiers::VoteCreditTierAction, }; @@ -153,6 +153,13 @@ pub enum PanelQuery { /// Action action: ShopCouponAction, }, + /// Fetch and update/modify shop holds + UpdateShopHolds { + /// Login token + login_token: String, + /// Action + action: ShopHoldAction, + }, /// Fetch and update/modify bot whitelist UpdateBotWhitelist { /// Login token diff --git a/src/panelapi/server.rs b/src/panelapi/server.rs index f1563e77..767d09ad 100644 --- a/src/panelapi/server.rs +++ b/src/panelapi/server.rs @@ -18,7 +18,7 @@ use crate::panelapi::types::{ ShopItemBenefitAction, }, staff_disciplinary::StaffDisciplinaryTypeAction, - vote_credit_tiers::{VoteCreditTier, VoteCreditTierAction}, + vote_credit_tiers::VoteCreditTierAction, webcore::InstanceConfig, }; use crate::rpc::core::{RPCHandle, RPCMethod}; @@ -34,6 +34,7 @@ use log::info; use sqlx::PgPool; use tower_http::cors::{Any, CorsLayer}; +use super::actions; use super::core::{AppState, Error}; use super::types::staff_members::StaffMemberAction; use super::types::staff_positions::StaffPositionAction; @@ -724,274 +725,8 @@ async fn query( login_token, action, } => { - let auth_data = super::auth::check_auth(&state.pool, &login_token) - .await - .map_err(Error::new)?; - - let user_perms = get_user_perms(&state.pool, &auth_data.user_id) + actions::updatevotecredittiers::update_vote_credit_tiers(&state, login_token, action) .await - .map_err(Error::new)? - .resolve(); - - match action { - VoteCreditTierAction::ListTiers => { - let rows = sqlx::query!( - "SELECT id, target_type, position, cents, votes, created_at FROM vote_credit_tiers ORDER BY position ASC" - ) - .fetch_all(&state.pool) - .await - .map_err(Error::new)?; - - let mut entries = Vec::new(); - - for row in rows { - entries.push(VoteCreditTier { - id: row.id, - target_type: row.target_type, - position: row.position, - cents: row.cents, - votes: row.votes, - created_at: row.created_at, - }); - } - - Ok((StatusCode::OK, Json(entries)).into_response()) - } - VoteCreditTierAction::CreateTier { - id, - position, - target_type, - cents, - votes, - } => { - if !perms::has_perm(&user_perms, &"vote_credit_tiers.create".into()) { - return Ok(( - StatusCode::FORBIDDEN, - "You do not have permission to create vote credit tiers [vote_credit_tiers.create]".to_string(), - ) - .into_response()); - } - - if cents < 0.0 { - return Ok(( - StatusCode::BAD_REQUEST, - "Cents cannot be lower than 0".to_string(), - ) - .into_response()); - } - - if votes < 0 { - return Ok(( - StatusCode::BAD_REQUEST, - "Votes cannot be lower than 0".to_string(), - ) - .into_response()); - } - - if target_type != "bot" && target_type != "server" { - return Ok(( - StatusCode::BAD_REQUEST, - "Target type must be either 'bot' or 'server'".to_string(), - ) - .into_response()); - } - - // Insert entry - let mut tx = state.pool.begin().await.map_err(Error::new)?; - sqlx::query!( - "INSERT INTO vote_credit_tiers (id, target_type, position, cents, votes) VALUES ($1, $2, $3, $4, $5)", - id, - target_type, - position, - cents, - votes, - ) - .execute(&mut *tx) - .await - .map_err(Error::new)?; - - // Now keep shifting positions until they are all unique - let mut index_a = position; - - loop { - let rows = sqlx::query!( - "SELECT id, position FROM vote_credit_tiers WHERE position = $1 AND id != $2", - index_a, - id, - ) - .fetch_all(&mut *tx) - .await - .map_err(Error::new)?; - - if rows.is_empty() { - break; - } - - let mut index_b = index_a + 1; - - for row in rows { - sqlx::query!( - "UPDATE vote_credit_tiers SET position = $1 WHERE id = $2", - index_b, - row.id, - ) - .execute(&mut *tx) - .await - .map_err(Error::new)?; - - index_b += 1; - } - - index_a = index_b; - } - - tx.commit().await.map_err(Error::new)?; - - Ok((StatusCode::NO_CONTENT, "").into_response()) - } - VoteCreditTierAction::EditTier { - id, - position, - target_type, - cents, - votes, - } => { - if !perms::has_perm(&user_perms, &"vote_credit_tiers.update".into()) { - return Ok(( - StatusCode::FORBIDDEN, - "You do not have permission to update vote credit tiers [vote_credit_tiers.update]".to_string(), - ) - .into_response()); - } - - // Check if entry already exists with same id - if sqlx::query!("SELECT COUNT(*) FROM vote_credit_tiers WHERE id = $1", id) - .fetch_one(&state.pool) - .await - .map_err(Error::new)? - .count - .unwrap_or(0) - == 0 - { - return Ok(( - StatusCode::BAD_REQUEST, - "Entry with same id does not already exist".to_string(), - ) - .into_response()); - } - - if cents < 0.0 { - return Ok(( - StatusCode::BAD_REQUEST, - "Cents cannot be lower than 0".to_string(), - ) - .into_response()); - } - - if votes < 0 { - return Ok(( - StatusCode::BAD_REQUEST, - "Votes cannot be lower than 0".to_string(), - ) - .into_response()); - } - - if target_type != "bot" && target_type != "server" { - return Ok(( - StatusCode::BAD_REQUEST, - "Target type must be either 'bot' or 'server'".to_string(), - ) - .into_response()); - } - - let mut tx = state.pool.begin().await.map_err(Error::new)?; - - // Update entry - sqlx::query!( - "UPDATE vote_credit_tiers SET position = $1, target_type = $2, cents = $3, votes = $4 WHERE id = $5", - position, - target_type, - cents, - votes, - id, - ) - .execute(&mut *tx) - .await - .map_err(Error::new)?; - - // Now keep shifting positions until they are all unique - let mut index_a = position; - - loop { - let rows = sqlx::query!( - "SELECT id, position FROM vote_credit_tiers WHERE position = $1 AND id != $2", - index_a, - id, - ) - .fetch_all(&mut *tx) - .await - .map_err(Error::new)?; - - if rows.is_empty() { - break; - } - - let mut index_b = index_a + 1; - - for row in rows { - sqlx::query!( - "UPDATE vote_credit_tiers SET position = $1 WHERE id = $2", - index_b, - row.id, - ) - .execute(&mut *tx) - .await - .map_err(Error::new)?; - - index_b += 1; - } - - index_a = index_b; - } - - tx.commit().await.map_err(Error::new)?; - - Ok((StatusCode::NO_CONTENT, "").into_response()) - } - VoteCreditTierAction::DeleteTier { id } => { - if !perms::has_perm(&user_perms, &"vote_credit_tiers.delete".into()) { - return Ok(( - StatusCode::FORBIDDEN, - "You do not have permission to delete vote credit tiers [vote_credit_tiers.delete]".to_string(), - ) - .into_response()); - } - - // Check if entry already exists with same vesion - if sqlx::query!("SELECT COUNT(*) FROM vote_credit_tiers WHERE id = $1", id) - .fetch_one(&state.pool) - .await - .map_err(Error::new)? - .count - .unwrap_or(0) - == 0 - { - return Ok(( - StatusCode::BAD_REQUEST, - "Entry with same id does not already exist".to_string(), - ) - .into_response()); - } - - // Delete entry - sqlx::query!("DELETE FROM vote_credit_tiers WHERE id = $1", id) - .execute(&state.pool) - .await - .map_err(Error::new)?; - - Ok((StatusCode::NO_CONTENT, "").into_response()) - } - } } PanelQuery::UpdateShopItems { login_token, @@ -1806,5 +1541,9 @@ async fn query( } } } + PanelQuery::UpdateShopHolds { + login_token, + action, + } => actions::updateshopholds::update_shop_holds(&state, login_token, action).await, } } diff --git a/src/panelapi/types/shop_items.rs b/src/panelapi/types/shop_items.rs index 20930626..e27422d1 100644 --- a/src/panelapi/types/shop_items.rs +++ b/src/panelapi/types/shop_items.rs @@ -308,3 +308,75 @@ pub enum ShopCouponAction { id: String, }, } + +/// Shop holds store items which are owned by entities +#[derive(Serialize, Deserialize, TS, Clone, ToSchema)] +#[ts(export, export_to = ".generated/ShopHold.ts")] +pub struct ShopHold { + #[ts(type = "string")] + /// The ID of the shop hold + pub id: sqlx::types::uuid::Uuid, + /// Target ID + pub target_id: String, + /// Target type + pub target_type: String, + /// Item + pub item: String, + /// Created at + pub created_at: chrono::DateTime, + /// Duration, in *seconds* + pub duration: Option, +} + +#[derive( + Serialize, + Deserialize, + ToSchema, + TS, + EnumString, + EnumVariantNames, + Display, + Clone, + PartialEq, + Default, +)] +#[ts(export, export_to = ".generated/ShopHoldAction.ts")] +pub enum ShopHoldAction { + /// List all current shop holds + #[default] + List, + + /// Create a new shop hold + Create { + /// Target ID + target_id: String, + /// Target type + target_type: String, + /// Item + item: String, + /// Duration, in *seconds* + duration: Option, + }, + + /// Edit a shop hold + Edit { + #[ts(type = "string")] + /// The ID of the shop hold + id: sqlx::types::uuid::Uuid, + /// Target ID + target_id: String, + /// Target type + target_type: String, + /// Item + item: String, + /// Duration, in *seconds* + duration: Option, + }, + + /// Deletes a shop hold + Delete { + #[ts(type = "string")] + /// The ID of the shop hold + id: sqlx::types::uuid::Uuid, + }, +} From 11685ad520ac6f4498c13eaccef95b3038ba22bc Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 5 Jun 2025 06:02:53 +0000 Subject: [PATCH 3/4] fix --- ...c351982239f51cb7f2c4a58790bbedc8d06ee.json | 14 ++++ ...8f2d6cd34031cb8c306e4cd700b8f05af78ba.json | 20 ------ ...e079b36fb432215b49da48ee0d08a8dbd8982.json | 21 ------ ...6a74d2f49fdb8775a1cfc2f0be2fde0c7eb0f.json | 18 +++++ ...159adf0b2a122529173810a3894f07aebe440.json | 68 ------------------- ...a3aa751f2630250c48144b35f4e286355e723.json | 20 ++++++ ...b7fd7b08d08ca113961b89ecf890fecba848.json} | 4 +- ...6246d336675c109c0d138d7c476cecb70820.json} | 6 +- ...8ef448d3b2feb083c9784e12c86ac6275f85.json} | 4 +- ...7998050d916fc6716105c8679e07dd499a190.json | 22 ------ ...18da6f68ed087299c2f9f91c3ddf52c267c06.json | 17 +++++ ...ff69a8077a0b2bf2cfa1e19e68a3d8ca4fe3f.json | 50 ++++++++++++++ ...0d19dd638f7c9ef37fadcab632c68363ebec7.json | 19 ------ ...d0322dc38245104c42d0e7a50591a4de715a.json} | 4 +- ...e55b0d537e7216272cc32fcdd600f6765903d.json | 15 ++++ src/panelapi/actions/baseanalytics.rs | 7 +- 16 files changed, 144 insertions(+), 165 deletions(-) create mode 100644 .sqlx/query-0ca9651eefd1476684f14042a86c351982239f51cb7f2c4a58790bbedc8d06ee.json delete mode 100644 .sqlx/query-17cf6be567251433b1b998e2daa8f2d6cd34031cb8c306e4cd700b8f05af78ba.json delete mode 100644 .sqlx/query-39656755b05bc7e9e7df4434c41e079b36fb432215b49da48ee0d08a8dbd8982.json create mode 100644 .sqlx/query-3bda474874986d84a0b0686bd436a74d2f49fdb8775a1cfc2f0be2fde0c7eb0f.json delete mode 100644 .sqlx/query-531be61ba714ff295807bd30cd9159adf0b2a122529173810a3894f07aebe440.json create mode 100644 .sqlx/query-5b39372139f293d9a1467882917a3aa751f2630250c48144b35f4e286355e723.json rename .sqlx/{query-b84a691ead056ae20cee9667bad552a044e1dea491f417c62817977de1051d0c.json => query-5c73d0745d726987c709f3ffb11fb7fd7b08d08ca113961b89ecf890fecba848.json} (75%) rename .sqlx/{query-d93496e8ee3f1ee7e5b63c827299adc62fb4f44c360ef519f7fe45e6388dbc90.json => query-9b9ecdcf9263e9f4c4b9a07567516246d336675c109c0d138d7c476cecb70820.json} (61%) rename .sqlx/{query-d9f0766cdd98a96e7dd5575969aa52b1cd497788ceac23ea55b060ad5365b39c.json => query-af7f9ab041db6964bbd784e450568ef448d3b2feb083c9784e12c86ac6275f85.json} (80%) delete mode 100644 .sqlx/query-c8ca01fe430cbfa0b19cbe049377998050d916fc6716105c8679e07dd499a190.json create mode 100644 .sqlx/query-cbabce4011abf74e3b2fe0858f518da6f68ed087299c2f9f91c3ddf52c267c06.json create mode 100644 .sqlx/query-cd1f9ac3ddfbb85c7b17e5971c6ff69a8077a0b2bf2cfa1e19e68a3d8ca4fe3f.json delete mode 100644 .sqlx/query-cfcd4226572d5b73b2285bac4780d19dd638f7c9ef37fadcab632c68363ebec7.json rename .sqlx/{query-63c9b866a1d4c52db542c4a976c18499ae20a193735d6f0c0eb43652ab4266d4.json => query-f51976d93ee27ff76309f40de74bd0322dc38245104c42d0e7a50591a4de715a.json} (50%) create mode 100644 .sqlx/query-f73e5554205487fd52622abdbc6e55b0d537e7216272cc32fcdd600f6765903d.json diff --git a/.sqlx/query-0ca9651eefd1476684f14042a86c351982239f51cb7f2c4a58790bbedc8d06ee.json b/.sqlx/query-0ca9651eefd1476684f14042a86c351982239f51cb7f2c4a58790bbedc8d06ee.json new file mode 100644 index 00000000..38a08f07 --- /dev/null +++ b/.sqlx/query-0ca9651eefd1476684f14042a86c351982239f51cb7f2c4a58790bbedc8d06ee.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM shop_holds WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "0ca9651eefd1476684f14042a86c351982239f51cb7f2c4a58790bbedc8d06ee" +} diff --git a/.sqlx/query-17cf6be567251433b1b998e2daa8f2d6cd34031cb8c306e4cd700b8f05af78ba.json b/.sqlx/query-17cf6be567251433b1b998e2daa8f2d6cd34031cb8c306e4cd700b8f05af78ba.json deleted file mode 100644 index e553bbcb..00000000 --- a/.sqlx/query-17cf6be567251433b1b998e2daa8f2d6cd34031cb8c306e4cd700b8f05af78ba.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT COUNT(*) FROM changelogs", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "count", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - null - ] - }, - "hash": "17cf6be567251433b1b998e2daa8f2d6cd34031cb8c306e4cd700b8f05af78ba" -} diff --git a/.sqlx/query-39656755b05bc7e9e7df4434c41e079b36fb432215b49da48ee0d08a8dbd8982.json b/.sqlx/query-39656755b05bc7e9e7df4434c41e079b36fb432215b49da48ee0d08a8dbd8982.json deleted file mode 100644 index 6f96b2e1..00000000 --- a/.sqlx/query-39656755b05bc7e9e7df4434c41e079b36fb432215b49da48ee0d08a8dbd8982.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE changelogs SET extra_description = $2, github_html = $3, prerelease = $4, added = $5, updated = $6, removed = $7, published = $8 WHERE version = $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text", - "Text", - "Bool", - "TextArray", - "TextArray", - "TextArray", - "Bool" - ] - }, - "nullable": [] - }, - "hash": "39656755b05bc7e9e7df4434c41e079b36fb432215b49da48ee0d08a8dbd8982" -} diff --git a/.sqlx/query-3bda474874986d84a0b0686bd436a74d2f49fdb8775a1cfc2f0be2fde0c7eb0f.json b/.sqlx/query-3bda474874986d84a0b0686bd436a74d2f49fdb8775a1cfc2f0be2fde0c7eb0f.json new file mode 100644 index 00000000..789a7c34 --- /dev/null +++ b/.sqlx/query-3bda474874986d84a0b0686bd436a74d2f49fdb8775a1cfc2f0be2fde0c7eb0f.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE shop_holds SET target_id = $1, target_type = $2, item = $3, duration = $4 WHERE id = $5", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text", + "Text", + "Interval", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "3bda474874986d84a0b0686bd436a74d2f49fdb8775a1cfc2f0be2fde0c7eb0f" +} diff --git a/.sqlx/query-531be61ba714ff295807bd30cd9159adf0b2a122529173810a3894f07aebe440.json b/.sqlx/query-531be61ba714ff295807bd30cd9159adf0b2a122529173810a3894f07aebe440.json deleted file mode 100644 index 8a2c90db..00000000 --- a/.sqlx/query-531be61ba714ff295807bd30cd9159adf0b2a122529173810a3894f07aebe440.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT version, added, updated, removed, github_html, created_at, extra_description, prerelease, published FROM changelogs ORDER BY version::semver DESC", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "version", - "type_info": "Text" - }, - { - "ordinal": 1, - "name": "added", - "type_info": "TextArray" - }, - { - "ordinal": 2, - "name": "updated", - "type_info": "TextArray" - }, - { - "ordinal": 3, - "name": "removed", - "type_info": "TextArray" - }, - { - "ordinal": 4, - "name": "github_html", - "type_info": "Text" - }, - { - "ordinal": 5, - "name": "created_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 6, - "name": "extra_description", - "type_info": "Text" - }, - { - "ordinal": 7, - "name": "prerelease", - "type_info": "Bool" - }, - { - "ordinal": 8, - "name": "published", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false, - false, - false, - true, - false, - false, - false, - false - ] - }, - "hash": "531be61ba714ff295807bd30cd9159adf0b2a122529173810a3894f07aebe440" -} diff --git a/.sqlx/query-5b39372139f293d9a1467882917a3aa751f2630250c48144b35f4e286355e723.json b/.sqlx/query-5b39372139f293d9a1467882917a3aa751f2630250c48144b35f4e286355e723.json new file mode 100644 index 00000000..232884f7 --- /dev/null +++ b/.sqlx/query-5b39372139f293d9a1467882917a3aa751f2630250c48144b35f4e286355e723.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT bot_id FROM bots WHERE (type = 'approved' OR type = 'certified') AND (last_stats_post IS NULL OR NOW() - last_stats_post > INTERVAL '3 days') AND (last_japi_update IS NULL OR NOW() - last_japi_update > INTERVAL '3 days') ORDER BY RANDOM() LIMIT 10", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "bot_id", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "5b39372139f293d9a1467882917a3aa751f2630250c48144b35f4e286355e723" +} diff --git a/.sqlx/query-b84a691ead056ae20cee9667bad552a044e1dea491f417c62817977de1051d0c.json b/.sqlx/query-5c73d0745d726987c709f3ffb11fb7fd7b08d08ca113961b89ecf890fecba848.json similarity index 75% rename from .sqlx/query-b84a691ead056ae20cee9667bad552a044e1dea491f417c62817977de1051d0c.json rename to .sqlx/query-5c73d0745d726987c709f3ffb11fb7fd7b08d08ca113961b89ecf890fecba848.json index cceb019f..8d7b7dae 100644 --- a/.sqlx/query-b84a691ead056ae20cee9667bad552a044e1dea491f417c62817977de1051d0c.json +++ b/.sqlx/query-5c73d0745d726987c709f3ffb11fb7fd7b08d08ca113961b89ecf890fecba848.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT bot_id, client_id, type, approximate_votes, shards, library, invite_clicks, clicks,\n servers, last_claimed, claimed_by, approval_note, short, invite FROM bots\n INNER JOIN internal_user_cache__discord discord_users ON bots.bot_id = discord_users.id\n WHERE bot_id = $1 OR client_id = $1 OR discord_users.username ILIKE $2 ORDER BY bots.created_at\n ", + "query": "\n SELECT bot_id, client_id, type, approximate_votes, shards, library, invite_clicks, clicks,\n servers, last_claimed, claimed_by, approval_note, short, invite FROM bots\n INNER JOIN internal_user_cache__discord discord_users ON bots.bot_id = discord_users.id\n WHERE bot_id = $1 OR client_id = $1 OR discord_users.username ILIKE $2 ORDER BY bots.created_at\n ", "describe": { "columns": [ { @@ -97,5 +97,5 @@ false ] }, - "hash": "b84a691ead056ae20cee9667bad552a044e1dea491f417c62817977de1051d0c" + "hash": "5c73d0745d726987c709f3ffb11fb7fd7b08d08ca113961b89ecf890fecba848" } diff --git a/.sqlx/query-d93496e8ee3f1ee7e5b63c827299adc62fb4f44c360ef519f7fe45e6388dbc90.json b/.sqlx/query-9b9ecdcf9263e9f4c4b9a07567516246d336675c109c0d138d7c476cecb70820.json similarity index 61% rename from .sqlx/query-d93496e8ee3f1ee7e5b63c827299adc62fb4f44c360ef519f7fe45e6388dbc90.json rename to .sqlx/query-9b9ecdcf9263e9f4c4b9a07567516246d336675c109c0d138d7c476cecb70820.json index 652277f3..d0980c2e 100644 --- a/.sqlx/query-d93496e8ee3f1ee7e5b63c827299adc62fb4f44c360ef519f7fe45e6388dbc90.json +++ b/.sqlx/query-9b9ecdcf9263e9f4c4b9a07567516246d336675c109c0d138d7c476cecb70820.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT COUNT(*) FROM changelogs WHERE version = $1", + "query": "SELECT COUNT(*) FROM shop_holds WHERE id = $1", "describe": { "columns": [ { @@ -11,12 +11,12 @@ ], "parameters": { "Left": [ - "Text" + "Uuid" ] }, "nullable": [ null ] }, - "hash": "d93496e8ee3f1ee7e5b63c827299adc62fb4f44c360ef519f7fe45e6388dbc90" + "hash": "9b9ecdcf9263e9f4c4b9a07567516246d336675c109c0d138d7c476cecb70820" } diff --git a/.sqlx/query-d9f0766cdd98a96e7dd5575969aa52b1cd497788ceac23ea55b060ad5365b39c.json b/.sqlx/query-af7f9ab041db6964bbd784e450568ef448d3b2feb083c9784e12c86ac6275f85.json similarity index 80% rename from .sqlx/query-d9f0766cdd98a96e7dd5575969aa52b1cd497788ceac23ea55b060ad5365b39c.json rename to .sqlx/query-af7f9ab041db6964bbd784e450568ef448d3b2feb083c9784e12c86ac6275f85.json index 7b877578..2ab45e4f 100644 --- a/.sqlx/query-d9f0766cdd98a96e7dd5575969aa52b1cd497788ceac23ea55b060ad5365b39c.json +++ b/.sqlx/query-af7f9ab041db6964bbd784e450568ef448d3b2feb083c9784e12c86ac6275f85.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT server_id, name, total_members, online_members, short, type, approximate_votes, invite_clicks,\n clicks, nsfw, tags, premium, claimed_by, last_claimed FROM servers\n WHERE server_id = $1 OR name ILIKE $2 ORDER BY created_at\n ", + "query": "\n SELECT server_id, name, total_members, online_members, short, type, approximate_votes, invite_clicks,\n clicks, nsfw, tags, premium, claimed_by, last_claimed FROM servers\n WHERE server_id = $1 OR name ILIKE $2 ORDER BY created_at\n ", "describe": { "columns": [ { @@ -97,5 +97,5 @@ true ] }, - "hash": "d9f0766cdd98a96e7dd5575969aa52b1cd497788ceac23ea55b060ad5365b39c" + "hash": "af7f9ab041db6964bbd784e450568ef448d3b2feb083c9784e12c86ac6275f85" } diff --git a/.sqlx/query-c8ca01fe430cbfa0b19cbe049377998050d916fc6716105c8679e07dd499a190.json b/.sqlx/query-c8ca01fe430cbfa0b19cbe049377998050d916fc6716105c8679e07dd499a190.json deleted file mode 100644 index 2ca4f2eb..00000000 --- a/.sqlx/query-c8ca01fe430cbfa0b19cbe049377998050d916fc6716105c8679e07dd499a190.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT popplio_token FROM staffpanel__authchain WHERE token = $1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "popplio_token", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false - ] - }, - "hash": "c8ca01fe430cbfa0b19cbe049377998050d916fc6716105c8679e07dd499a190" -} diff --git a/.sqlx/query-cbabce4011abf74e3b2fe0858f518da6f68ed087299c2f9f91c3ddf52c267c06.json b/.sqlx/query-cbabce4011abf74e3b2fe0858f518da6f68ed087299c2f9f91c3ddf52c267c06.json new file mode 100644 index 00000000..bbbb6ce4 --- /dev/null +++ b/.sqlx/query-cbabce4011abf74e3b2fe0858f518da6f68ed087299c2f9f91c3ddf52c267c06.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO shop_holds (target_id, target_type, item, duration) VALUES ($1, $2, $3, $4)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text", + "Text", + "Interval" + ] + }, + "nullable": [] + }, + "hash": "cbabce4011abf74e3b2fe0858f518da6f68ed087299c2f9f91c3ddf52c267c06" +} diff --git a/.sqlx/query-cd1f9ac3ddfbb85c7b17e5971c6ff69a8077a0b2bf2cfa1e19e68a3d8ca4fe3f.json b/.sqlx/query-cd1f9ac3ddfbb85c7b17e5971c6ff69a8077a0b2bf2cfa1e19e68a3d8ca4fe3f.json new file mode 100644 index 00000000..7cfde575 --- /dev/null +++ b/.sqlx/query-cd1f9ac3ddfbb85c7b17e5971c6ff69a8077a0b2bf2cfa1e19e68a3d8ca4fe3f.json @@ -0,0 +1,50 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, target_id, target_type, item, created_at, duration FROM shop_holds ORDER BY created_at ASC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "target_id", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "target_type", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "item", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "duration", + "type_info": "Interval" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + true + ] + }, + "hash": "cd1f9ac3ddfbb85c7b17e5971c6ff69a8077a0b2bf2cfa1e19e68a3d8ca4fe3f" +} diff --git a/.sqlx/query-cfcd4226572d5b73b2285bac4780d19dd638f7c9ef37fadcab632c68363ebec7.json b/.sqlx/query-cfcd4226572d5b73b2285bac4780d19dd638f7c9ef37fadcab632c68363ebec7.json deleted file mode 100644 index abc626bb..00000000 --- a/.sqlx/query-cfcd4226572d5b73b2285bac4780d19dd638f7c9ef37fadcab632c68363ebec7.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO changelogs (version, extra_description, prerelease, added, updated, removed) VALUES ($1, $2, $3, $4, $5, $6)", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text", - "Bool", - "TextArray", - "TextArray", - "TextArray" - ] - }, - "nullable": [] - }, - "hash": "cfcd4226572d5b73b2285bac4780d19dd638f7c9ef37fadcab632c68363ebec7" -} diff --git a/.sqlx/query-63c9b866a1d4c52db542c4a976c18499ae20a193735d6f0c0eb43652ab4266d4.json b/.sqlx/query-f51976d93ee27ff76309f40de74bd0322dc38245104c42d0e7a50591a4de715a.json similarity index 50% rename from .sqlx/query-63c9b866a1d4c52db542c4a976c18499ae20a193735d6f0c0eb43652ab4266d4.json rename to .sqlx/query-f51976d93ee27ff76309f40de74bd0322dc38245104c42d0e7a50591a4de715a.json index cc8671f0..af05f042 100644 --- a/.sqlx/query-63c9b866a1d4c52db542c4a976c18499ae20a193735d6f0c0eb43652ab4266d4.json +++ b/.sqlx/query-f51976d93ee27ff76309f40de74bd0322dc38245104c42d0e7a50591a4de715a.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "DELETE FROM changelogs WHERE version = $1", + "query": "UPDATE bots SET last_japi_update = NOW() WHERE bot_id = $1", "describe": { "columns": [], "parameters": { @@ -10,5 +10,5 @@ }, "nullable": [] }, - "hash": "63c9b866a1d4c52db542c4a976c18499ae20a193735d6f0c0eb43652ab4266d4" + "hash": "f51976d93ee27ff76309f40de74bd0322dc38245104c42d0e7a50591a4de715a" } diff --git a/.sqlx/query-f73e5554205487fd52622abdbc6e55b0d537e7216272cc32fcdd600f6765903d.json b/.sqlx/query-f73e5554205487fd52622abdbc6e55b0d537e7216272cc32fcdd600f6765903d.json new file mode 100644 index 00000000..41ac57c9 --- /dev/null +++ b/.sqlx/query-f73e5554205487fd52622abdbc6e55b0d537e7216272cc32fcdd600f6765903d.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE bots SET last_japi_update = NOW(), servers = $1 WHERE bot_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4", + "Text" + ] + }, + "nullable": [] + }, + "hash": "f73e5554205487fd52622abdbc6e55b0d537e7216272cc32fcdd600f6765903d" +} diff --git a/src/panelapi/actions/baseanalytics.rs b/src/panelapi/actions/baseanalytics.rs index 5c75256a..873f74ff 100644 --- a/src/panelapi/actions/baseanalytics.rs +++ b/src/panelapi/actions/baseanalytics.rs @@ -32,11 +32,6 @@ pub async fn base_analytics(state: &AppState, login_token: String) -> Result Result Date: Thu, 5 Jun 2025 06:12:40 +0000 Subject: [PATCH 4/4] fix --- src/panelapi/server.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/panelapi/server.rs b/src/panelapi/server.rs index 767d09ad..5ab56138 100644 --- a/src/panelapi/server.rs +++ b/src/panelapi/server.rs @@ -115,7 +115,7 @@ pub async fn init_panelapi(pool: PgPool, cache_http: botox::cache::CacheHttpImpl .allow_headers(Any), ); - let addr = format!("127.0.0.1:{}", crate::config::CONFIG.server_port.get()); + let addr = format!("0.0.0.0:{}", crate::config::CONFIG.server_port.get()); info!("Starting server on {}", addr); let listener = tokio::net::TcpListener::bind(addr)