diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 9f60643..20879a8 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -2262,9 +2262,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "minecraft-pinger" -version = "0.1.8" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec4292bdf0a0ae6ee907c0f9bb8c61162e78b083613b48087afe7afc14cc88e8" +checksum = "91142aeae1add7a2637dbecd8ac71afdc1fe13a06ea03a6ce073a7902a478f04" dependencies = [ "bytes", "hickory-resolver", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index d4ccb30..113b1a8 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -4,7 +4,7 @@ members = ["worker", "rest-api"] [workspace.dependencies] repository = { path = "./crates/repository" } -minecraft-pinger = "0.1.8" +minecraft-pinger = "0.1.10" log = "0.4.*" env_logger = "0.11.*" diff --git a/backend/crates/repository/src/duplicate_detection.rs b/backend/crates/repository/src/duplicate_detection.rs index 91abf03..7929526 100644 --- a/backend/crates/repository/src/duplicate_detection.rs +++ b/backend/crates/repository/src/duplicate_detection.rs @@ -1,12 +1,12 @@ -use sha2::{Sha256, Digest}; -use hickory_resolver::TokioResolver; -use std::net::IpAddr; -use hickory_proto::rr::RData; -use serde_json::Value; -use std::sync::LazyLock; use crate::error::RepositoryError; use crate::models::server::Server; use crate::repository::Repository; +use hickory_proto::rr::RData; +use hickory_resolver::TokioResolver; +use serde_json::Value; +use sha2::{Digest, Sha256}; +use std::net::IpAddr; +use std::sync::LazyLock; const SIGNAL_WEIGHTS_FAVICON: u32 = 50; const SIGNAL_WEIGHTS_ENDPOINT: u32 = 40; @@ -16,9 +16,8 @@ const SIGNAL_WEIGHTS_VERSION: u32 = 8; const DUPLICATE_THRESHOLD: u32 = 75; const SHARED_ENDPOINT_LIMIT: u32 = 2; -static DNS_RESOLVER: LazyLock> = LazyLock::new(|| { - TokioResolver::builder_tokio().ok()?.build().ok() -}); +static DNS_RESOLVER: LazyLock> = + LazyLock::new(|| TokioResolver::builder_tokio().ok()?.build().ok()); #[derive(Debug, Clone)] pub struct ServerFingerprint { @@ -44,7 +43,9 @@ impl DuplicateDetectionService { Some(idx) => favicon[idx + 1..].trim(), None => favicon.trim(), }; - if base64.is_empty() { return None; } + if base64.is_empty() { + return None; + } let mut hasher = Sha256::new(); hasher.update(base64.as_bytes()); @@ -98,7 +99,9 @@ impl DuplicateDetectionService { normalized.push_str(word); } - if normalized.chars().count() < 4 { return None; } + if normalized.chars().count() < 4 { + return None; + } let mut hasher = Sha256::new(); hasher.update(normalized.as_bytes()); @@ -118,13 +121,17 @@ impl DuplicateDetectionService { let mut target_port = port; if let Ok(srv) = resolver.srv_lookup(&srv_name).await { - let best_srv = srv.answers().iter().filter_map(|record| { - if let RData::SRV(srv) = &record.data { - Some(srv) - } else { - None - } - }).min_by_key(|r| r.priority); + let best_srv = srv + .answers() + .iter() + .filter_map(|record| { + if let RData::SRV(srv) = &record.data { + Some(srv) + } else { + None + } + }) + .min_by_key(|r| r.priority); if let Some(best) = best_srv { target_host = best.target.to_utf8().trim_end_matches('.').to_string(); @@ -162,7 +169,9 @@ impl DuplicateDetectionService { let mut endpoint_trusted = false; if let Some(endpoint) = resolved_endpoint { - let count = repository.count_resolved_endpoints(endpoint, exclude_id).await?; + let count = repository + .count_resolved_endpoints(endpoint, exclude_id) + .await?; endpoint_trusted = count < SHARED_ENDPOINT_LIMIT; } @@ -174,11 +183,17 @@ impl DuplicateDetectionService { return Ok(None); } - let candidates = repository.find_servers( - favicon_hash.as_deref(), - if endpoint_trusted { resolved_endpoint.as_deref() } else { None }, - motd_hash.as_deref(), - ).await?; + let candidates = repository + .find_servers( + favicon_hash.as_deref(), + if endpoint_trusted { + resolved_endpoint.as_deref() + } else { + None + }, + motd_hash.as_deref(), + ) + .await?; let mut best: Option = None; @@ -233,4 +248,4 @@ impl DuplicateDetectionService { Ok(best) } -} \ No newline at end of file +} diff --git a/backend/crates/repository/src/error.rs b/backend/crates/repository/src/error.rs index 28063c1..2e5772c 100644 --- a/backend/crates/repository/src/error.rs +++ b/backend/crates/repository/src/error.rs @@ -8,7 +8,7 @@ pub enum RepositoryError { SQL(#[from] Error), #[error("SQLx migration error : {0}")] - Migration(#[from] MigrateError) + Migration(#[from] MigrateError), } impl RepositoryError { @@ -19,4 +19,4 @@ impl RepositoryError { false } } -} \ No newline at end of file +} diff --git a/backend/crates/repository/src/lib.rs b/backend/crates/repository/src/lib.rs index 6b286bb..136748c 100644 --- a/backend/crates/repository/src/lib.rs +++ b/backend/crates/repository/src/lib.rs @@ -1,5 +1,5 @@ -pub mod models; -pub mod repository; pub mod duplicate_detection; +pub mod error; +pub mod models; pub mod postgres; -pub mod error; \ No newline at end of file +pub mod repository; diff --git a/backend/crates/repository/src/models/alert.rs b/backend/crates/repository/src/models/alert.rs index 917f755..1e87819 100644 --- a/backend/crates/repository/src/models/alert.rs +++ b/backend/crates/repository/src/models/alert.rs @@ -18,11 +18,11 @@ pub struct Alert { pub id: u32, pub user_id: String, pub server_id: u32, - + pub alert_type: AlertType, pub player_threshold: Option, pub is_active: bool, - + pub created_at: OffsetDateTime, } @@ -31,11 +31,11 @@ pub struct AlertRow { pub id: i32, pub user_id: String, pub server_id: i32, - + pub alert_type: AlertType, pub player_threshold: Option, pub is_active: bool, - + pub created_at: OffsetDateTime, } @@ -57,7 +57,7 @@ impl From for Alert { pub struct DraftAlert { pub user_id: String, pub server_id: u32, - + pub alert_type: AlertType, pub player_threshold: Option, pub is_active: bool, diff --git a/backend/crates/repository/src/models/mod.rs b/backend/crates/repository/src/models/mod.rs index cb3ab5b..b8fb69a 100644 --- a/backend/crates/repository/src/models/mod.rs +++ b/backend/crates/repository/src/models/mod.rs @@ -1,5 +1,5 @@ +pub mod alert; +pub mod mojang; pub mod record; pub mod server; -pub mod alert; pub mod web_push; -pub mod mojang; diff --git a/backend/crates/repository/src/models/record.rs b/backend/crates/repository/src/models/record.rs index e12aca7..9372688 100644 --- a/backend/crates/repository/src/models/record.rs +++ b/backend/crates/repository/src/models/record.rs @@ -1,8 +1,8 @@ use serde::{Deserialize, Serialize}; -use time::{OffsetDateTime}; +use time::OffsetDateTime; #[derive(Serialize, Deserialize)] -pub struct RecordData(pub Vec, pub Vec); +pub struct RecordData(pub Vec, pub Vec); impl RecordData { pub fn into_binary(self) -> Vec { @@ -10,7 +10,7 @@ impl RecordData { let len = timestamps.len() as u32; let mut bytes = Vec::with_capacity(4 + 8 + (len as usize * 8)); - + // Write the number of items bytes.extend_from_slice(&len.to_le_bytes()); @@ -46,5 +46,5 @@ pub struct Record { #[serde(with = "time::serde::timestamp")] pub date: OffsetDateTime, - pub value: u32 + pub value: i32, } diff --git a/backend/crates/repository/src/models/server.rs b/backend/crates/repository/src/models/server.rs index 229814d..bea3c72 100644 --- a/backend/crates/repository/src/models/server.rs +++ b/backend/crates/repository/src/models/server.rs @@ -25,7 +25,7 @@ pub struct Server { #[serde(skip_serializing_if = "Option::is_none")] pub last_favicon: Option, pub last_status: Option, - pub last_connected: Option, + pub last_connected: Option, pub last_version: Option, pub last_max_players: Option, pub last_motd: Option, @@ -91,9 +91,11 @@ impl From for Server { last_favicon: row.last_favicon, last_status: row.last_status, - last_connected: row.last_connected.map(|v| v as u32), + last_connected: row.last_connected, last_version: row.last_version.map(|v| v.to_string()), - last_motd: row.last_motd.as_deref() + last_motd: row + .last_motd + .as_deref() .and_then(|s| serde_json::from_str(s).ok()), last_max_players: row.last_max_players, last_ping_time: row.last_ping_time.map(|v| v as u32), @@ -122,7 +124,7 @@ pub enum ServerStatus { #[serde(rename_all = "lowercase")] pub enum ServerType { Java, - Bedrock + Bedrock, } #[derive(Serialize, Deserialize, Debug)] diff --git a/backend/crates/repository/src/postgres.rs b/backend/crates/repository/src/postgres.rs index 3ba8d1d..07c5ce9 100644 --- a/backend/crates/repository/src/postgres.rs +++ b/backend/crates/repository/src/postgres.rs @@ -1,18 +1,20 @@ use std::collections::HashMap; -use async_trait::async_trait; -use sqlx::{PgPool, QueryBuilder, Row}; -use sqlx::postgres::PgPoolOptions; -use time::{Duration, OffsetDateTime}; -use crate::models::record::{Record, RecordData}; -use crate::models::server::{Server, ServerRow, DraftServer}; +use crate::error::RepositoryError; use crate::models::alert::{Alert, AlertRow, DraftAlert}; use crate::models::mojang::MojangApiDowntime; -use crate::models::web_push::{WebPushSubscription, WebPushSubscriptionRow, DraftWebPushSubscription}; +use crate::models::record::{Record, RecordData}; +use crate::models::server::{DraftServer, Server, ServerRow}; +use crate::models::web_push::{ + DraftWebPushSubscription, WebPushSubscription, WebPushSubscriptionRow, +}; use crate::repository::Repository; +use async_trait::async_trait; use futures::stream::StreamExt; -use log::{info}; -use crate::error::RepositoryError; +use log::info; +use sqlx::postgres::PgPoolOptions; +use sqlx::{PgPool, QueryBuilder, Row}; +use time::{Duration, OffsetDateTime}; #[derive(Clone)] pub struct PostgresRepository { @@ -34,9 +36,7 @@ impl PostgresRepository { info!("PostgreSQL connection success !"); let repository = PostgresRepository::new(pool); - repository.initialize() - .await - .map_err(|e| e.to_string())?; + repository.initialize().await.map_err(|e| e.to_string())?; info!("Initialized successfully!"); Ok(repository) @@ -50,7 +50,8 @@ impl Repository for PostgresRepository { return Ok(()); } - let mut query_builder = QueryBuilder::new("INSERT INTO ping_records (server_id, date, value) "); + let mut query_builder = + QueryBuilder::new("INSERT INTO ping_records (server_id, date, value) "); query_builder.push_values(records, |mut b, record| { b.push_bind(record.server_id as i32) @@ -59,18 +60,21 @@ impl Repository for PostgresRepository { }); let query = query_builder.build(); - query.execute(&self.pool) - .await - .map_err(|e| e.to_string())?; + query.execute(&self.pool).await.map_err(|e| e.to_string())?; Ok(()) } - async fn get_pings(&self, server_id: u32, from: OffsetDateTime, to: Option) -> Result { + async fn get_pings( + &self, + server_id: u32, + from: OffsetDateTime, + to: Option, + ) -> Result { let mut query_builder = QueryBuilder::new( "SELECT date, value FROM ping_records - WHERE server_id = " + WHERE server_id = ", ); query_builder.push_bind(server_id as i32); query_builder.push(" AND date >= ").push_bind(from); @@ -94,16 +98,19 @@ impl Repository for PostgresRepository { let value_i32: i32 = row.get("value"); dates.push(date.unix_timestamp()); - values.push(value_i32 as u32); + values.push(value_i32); } Ok(RecordData(dates, values)) } - async fn get_last_pings_for_servers(&self, server_ids: &[u32]) -> Result, RepositoryError> { + async fn get_last_pings_for_servers( + &self, + server_ids: &[u32], + ) -> Result, RepositoryError> { let ids: Vec = server_ids.iter().map(|&id| id as i32).collect(); let from = OffsetDateTime::now_utc() - Duration::days(1); - + let records = sqlx::query( "SELECT server_id, @@ -111,7 +118,7 @@ impl Repository for PostgresRepository { agg_value FROM ping_records_5m_agg WHERE server_id = ANY($1) AND time_bucket >= $2 - ORDER BY server_id, time_bucket ASC" + ORDER BY server_id, time_bucket ASC", ) .bind(&ids) .bind(from) @@ -119,20 +126,20 @@ impl Repository for PostgresRepository { .await?; let mut map: HashMap = HashMap::with_capacity(ids.len()); - + for row in records { let server_id: u32 = row.get::("server_id") as u32; let date: OffsetDateTime = row.get("time_bucket"); - let value: u32 = row.get::("agg_value") as u32; - + let value = row.get::("agg_value"); + let uplot_data = map .entry(server_id) .or_insert_with(|| RecordData(Vec::new(), Vec::new())); - + uplot_data.0.push(date.unix_timestamp()); uplot_data.1.push(value); } - + Ok(map) } @@ -156,7 +163,8 @@ impl Repository for PostgresRepository { } async fn update_server(&self, server: &Server) -> Result<(), RepositoryError> { - sqlx::query("UPDATE servers SET + sqlx::query( + "UPDATE servers SET last_favicon = $1, last_status = $2, last_connected = $3, @@ -175,32 +183,33 @@ impl Repository for PostgresRepository { last_protocol_version = $15, ip = $16, port = $17 - WHERE id = $18") - .bind(server.last_favicon.clone()) - .bind(server.last_status.clone()) - .bind(server.last_connected.map(|v| v as i32)) - .bind(server.last_version.clone()) - .bind(server.last_max_players.clone()) - .bind(server.last_motd.clone()) - .bind(server.favicon_hash.clone()) - .bind(server.motd_hash.clone()) - .bind(server.resolved_endpoint.clone()) - .bind(server.hidden) - .bind(server.name.clone()) - .bind(server.last_ping_time.map(|v| v as i32)) - .bind(server.last_sample.clone()) - .bind(server.forced_favicon) - .bind(server.last_protocol_version) - .bind(server.ip.clone()) - .bind(server.port as i32) - .bind(server.id as i32) - .execute(&self.pool) - .await?; + WHERE id = $18", + ) + .bind(server.last_favicon.clone()) + .bind(server.last_status.clone()) + .bind(server.last_connected.map(|v| v as i32)) + .bind(server.last_version.clone()) + .bind(server.last_max_players.clone()) + .bind(server.last_motd.clone()) + .bind(server.favicon_hash.clone()) + .bind(server.motd_hash.clone()) + .bind(server.resolved_endpoint.clone()) + .bind(server.hidden) + .bind(server.name.clone()) + .bind(server.last_ping_time.map(|v| v as i32)) + .bind(server.last_sample.clone()) + .bind(server.forced_favicon) + .bind(server.last_protocol_version) + .bind(server.ip.clone()) + .bind(server.port as i32) + .bind(server.id as i32) + .execute(&self.pool) + .await?; Ok(()) } - async fn update_servers(&self, servers: &Vec) -> Result<(), RepositoryError> { + async fn update_servers(&self, servers: &Vec) -> Result<(), RepositoryError> { if servers.is_empty() { return Ok(()); } @@ -319,17 +328,21 @@ impl Repository for PostgresRepository { Ok(rs) } - async fn get_server_without_favicon(&self, server_id: u32) -> Result, RepositoryError> { + async fn get_server_without_favicon( + &self, + server_id: u32, + ) -> Result, RepositoryError> { let result: Option = sqlx::query_as( "SELECT id, name, user_id, ip, port, type, hidden, registered_date, forced_favicon, NULL::text AS last_favicon, last_status, last_connected, last_version, last_max_players, last_motd, last_ping_time, last_sample, last_protocol_version, favicon_hash, motd_hash, resolved_endpoint - FROM servers WHERE id = $1") - .bind(server_id as i32) - .fetch_optional(&self.pool) - .await?; + FROM servers WHERE id = $1", + ) + .bind(server_id as i32) + .fetch_optional(&self.pool) + .await?; Ok(result.map(|r| r.into())) } @@ -341,9 +354,10 @@ impl Repository for PostgresRepository { last_status, last_connected, last_version, last_max_players, last_motd, last_ping_time, last_sample, last_protocol_version, favicon_hash, motd_hash, resolved_endpoint - FROM servers") - .fetch_all(&self.pool) - .await?; + FROM servers", + ) + .fetch_all(&self.pool) + .await?; let mut rs: Vec = Vec::new(); for row in rows { @@ -353,17 +367,21 @@ impl Repository for PostgresRepository { Ok(rs) } - async fn get_servers_of_user_without_favicon(&self, user_id: String) -> Result, RepositoryError> { + async fn get_servers_of_user_without_favicon( + &self, + user_id: String, + ) -> Result, RepositoryError> { let rows: Vec = sqlx::query_as( "SELECT id, name, user_id, ip, port, type, hidden, registered_date, forced_favicon, NULL::text AS last_favicon, last_status, last_connected, last_version, last_max_players, last_motd, last_ping_time, last_sample, last_protocol_version, favicon_hash, motd_hash, resolved_endpoint - FROM servers WHERE user_id = $1") - .bind(user_id) - .fetch_all(&self.pool) - .await?; + FROM servers WHERE user_id = $1", + ) + .bind(user_id) + .fetch_all(&self.pool) + .await?; let mut rs: Vec = Vec::new(); for row in rows { @@ -373,7 +391,12 @@ impl Repository for PostgresRepository { Ok(rs) } - async fn find_servers(&self, favicon_hash: Option<&str>, resolved_endpoint: Option<&str>, motd_hash: Option<&str>) -> Result, RepositoryError> { + async fn find_servers( + &self, + favicon_hash: Option<&str>, + resolved_endpoint: Option<&str>, + motd_hash: Option<&str>, + ) -> Result, RepositoryError> { let mut query = QueryBuilder::new("SELECT * FROM servers WHERE 1=0"); if let Some(h) = favicon_hash { @@ -389,22 +412,26 @@ impl Repository for PostgresRepository { query.push_bind(h); } - let rows: Vec = query.build_query_as() - .fetch_all(&self.pool) - .await?; + let rows: Vec = query.build_query_as().fetch_all(&self.pool).await?; Ok(rows.into_iter().map(|r| r.into()).collect()) } - async fn count_resolved_endpoints(&self, resolved_endpoint: &str, exclude_id: Option) -> Result { - let mut query = QueryBuilder::new("SELECT COUNT(*) FROM servers WHERE resolved_endpoint = "); + async fn count_resolved_endpoints( + &self, + resolved_endpoint: &str, + exclude_id: Option, + ) -> Result { + let mut query = + QueryBuilder::new("SELECT COUNT(*) FROM servers WHERE resolved_endpoint = "); query.push_bind(resolved_endpoint); if let Some(id) = exclude_id { query.push(" AND id != "); query.push_bind(id as i32); } - let row: (i64,) = query.build_query_as::<(i64,)>() + let row: (i64,) = query + .build_query_as::<(i64,)>() .fetch_one(&self.pool) .await?; @@ -415,7 +442,7 @@ impl Repository for PostgresRepository { let row: AlertRow = sqlx::query_as( "INSERT INTO alerts (user_id, server_id, alert_type, player_threshold, is_active) VALUES ($1, $2, $3, $4, $5) - RETURNING *" + RETURNING *", ) .bind(alert.user_id) .bind(alert.server_id as i32) @@ -429,21 +456,17 @@ impl Repository for PostgresRepository { } async fn delete_alert(&self, alert_id: u32, user_id: String) -> Result<(), RepositoryError> { - sqlx::query( - "DELETE FROM alerts WHERE id = $1 AND user_id = $2" - ) - .bind(alert_id as i32) - .bind(user_id) - .execute(&self.pool) - .await?; + sqlx::query("DELETE FROM alerts WHERE id = $1 AND user_id = $2") + .bind(alert_id as i32) + .bind(user_id) + .execute(&self.pool) + .await?; Ok(()) } async fn list_alerts_for_user(&self, user_id: String) -> Result, RepositoryError> { - let rows: Vec = sqlx::query_as( - "SELECT * FROM alerts WHERE user_id = $1" - ) + let rows: Vec = sqlx::query_as("SELECT * FROM alerts WHERE user_id = $1") .bind(user_id) .fetch_all(&self.pool) .await?; @@ -452,31 +475,32 @@ impl Repository for PostgresRepository { } async fn list_alerts_for_server(&self, server_id: u32) -> Result, RepositoryError> { - let rows: Vec = sqlx::query_as( - "SELECT * FROM alerts WHERE server_id = $1" - ) - .bind(server_id as i32) - .fetch_all(&self.pool) - .await?; + let rows: Vec = sqlx::query_as("SELECT * FROM alerts WHERE server_id = $1") + .bind(server_id as i32) + .fetch_all(&self.pool) + .await?; Ok(rows.into_iter().map(|r| r.into()).collect()) } - async fn get_active_alerts_for_servers(&self, server_ids: &[u32]) -> Result, RepositoryError> { + async fn get_active_alerts_for_servers( + &self, + server_ids: &[u32], + ) -> Result, RepositoryError> { let ids: Vec = server_ids.iter().map(|&id| id as i32).collect(); - let rows: Vec = sqlx::query_as( - "SELECT * FROM alerts WHERE server_id = ANY($1) AND is_active = TRUE" - ) - .bind(&ids) - .fetch_all(&self.pool) - .await?; + let rows: Vec = + sqlx::query_as("SELECT * FROM alerts WHERE server_id = ANY($1) AND is_active = TRUE") + .bind(&ids) + .fetch_all(&self.pool) + .await?; - Ok(rows.into_iter() - .map(|r| r.into()) - .collect()) + Ok(rows.into_iter().map(|r| r.into()).collect()) } - async fn create_subscription(&self, subscription: DraftWebPushSubscription) -> Result { + async fn create_subscription( + &self, + subscription: DraftWebPushSubscription, + ) -> Result { let row: WebPushSubscriptionRow = sqlx::query_as( "INSERT INTO web_push_subscriptions (user_id, endpoint, p256dh, auth) VALUES ($1, $2, $3, $4) @@ -485,7 +509,7 @@ impl Repository for PostgresRepository { p256dh = EXCLUDED.p256dh, auth = EXCLUDED.auth, created_at = NOW() - RETURNING *" + RETURNING *", ) .bind(subscription.user_id) .bind(subscription.endpoint) @@ -497,36 +521,38 @@ impl Repository for PostgresRepository { Ok(row.into()) } - async fn delete_subscription(&self, endpoint: &str, user_id: &str) -> Result<(), RepositoryError> { - sqlx::query( - "DELETE FROM web_push_subscriptions WHERE endpoint = $1 AND user_id = $2" - ) - .bind(endpoint) - .bind(user_id) - .execute(&self.pool) - .await?; + async fn delete_subscription( + &self, + endpoint: &str, + user_id: &str, + ) -> Result<(), RepositoryError> { + sqlx::query("DELETE FROM web_push_subscriptions WHERE endpoint = $1 AND user_id = $2") + .bind(endpoint) + .bind(user_id) + .execute(&self.pool) + .await?; Ok(()) } async fn delete_stale_subscription(&self, endpoint: &str) -> Result<(), RepositoryError> { - sqlx::query( - "DELETE FROM web_push_subscriptions WHERE endpoint = $1" - ) - .bind(endpoint) - .execute(&self.pool) - .await?; + sqlx::query("DELETE FROM web_push_subscriptions WHERE endpoint = $1") + .bind(endpoint) + .execute(&self.pool) + .await?; Ok(()) } - async fn get_subscriptions_for_users(&self, user_ids: &[String]) -> Result, RepositoryError> { - let rows: Vec = sqlx::query_as( - "SELECT * FROM web_push_subscriptions WHERE user_id = ANY($1)" - ) - .bind(user_ids) - .fetch_all(&self.pool) - .await?; + async fn get_subscriptions_for_users( + &self, + user_ids: &[String], + ) -> Result, RepositoryError> { + let rows: Vec = + sqlx::query_as("SELECT * FROM web_push_subscriptions WHERE user_id = ANY($1)") + .bind(user_ids) + .fetch_all(&self.pool) + .await?; Ok(rows.into_iter().map(|r| r.into()).collect()) } @@ -569,7 +595,10 @@ impl Repository for PostgresRepository { Ok(()) } - async fn start_mojang_api_downtime(&self, start_time: OffsetDateTime) -> Result<(), RepositoryError> { + async fn start_mojang_api_downtime( + &self, + start_time: OffsetDateTime, + ) -> Result<(), RepositoryError> { sqlx::query("INSERT INTO mojang_api_downtimes (start_time) VALUES ($1)") .bind(start_time) .execute(&self.pool) @@ -577,7 +606,10 @@ impl Repository for PostgresRepository { Ok(()) } - async fn end_mojang_api_downtime(&self, end_time: OffsetDateTime) -> Result<(), RepositoryError> { + async fn end_mojang_api_downtime( + &self, + end_time: OffsetDateTime, + ) -> Result<(), RepositoryError> { sqlx::query("UPDATE mojang_api_downtimes SET end_time = $1 WHERE end_time IS NULL") .bind(end_time) .execute(&self.pool) @@ -585,7 +617,10 @@ impl Repository for PostgresRepository { Ok(()) } - async fn get_mojang_api_downtimes(&self, limit: u32) -> Result, RepositoryError> { + async fn get_mojang_api_downtimes( + &self, + limit: u32, + ) -> Result, RepositoryError> { let downtimes = sqlx::query_as::<_, MojangApiDowntime>("SELECT id, start_time, end_time FROM mojang_api_downtimes ORDER BY start_time DESC LIMIT $1") .bind(limit as i64) .fetch_all(&self.pool) @@ -594,9 +629,7 @@ impl Repository for PostgresRepository { } async fn initialize(&self) -> Result<(), RepositoryError> { - sqlx::migrate!("./migrations") - .run(&self.pool) - .await?; + sqlx::migrate!("./migrations").run(&self.pool).await?; Ok(()) } diff --git a/backend/crates/repository/src/repository.rs b/backend/crates/repository/src/repository.rs index d9e97b5..7a2d935 100644 --- a/backend/crates/repository/src/repository.rs +++ b/backend/crates/repository/src/repository.rs @@ -1,21 +1,29 @@ use std::collections::HashMap; -use crate::models::record::{Record, RecordData}; -use crate::models::server::{Server, DraftServer}; use crate::models::alert::{Alert, DraftAlert}; use crate::models::mojang::MojangApiDowntime; -use crate::models::web_push::{WebPushSubscription, DraftWebPushSubscription}; +use crate::models::record::{Record, RecordData}; +use crate::models::server::{DraftServer, Server}; +use crate::models::web_push::{DraftWebPushSubscription, WebPushSubscription}; use async_trait::async_trait; -use time::{OffsetDateTime}; use crate::error::RepositoryError; +use time::OffsetDateTime; #[async_trait] pub trait Repository: Send + Sync { // Pings async fn save_pings(&self, records: &Vec) -> Result<(), String>; - async fn get_pings(&self, server_id: u32, from: OffsetDateTime, to: Option) -> Result; - async fn get_last_pings_for_servers(&self, server_ids: &[u32]) -> Result, RepositoryError>; + async fn get_pings( + &self, + server_id: u32, + from: OffsetDateTime, + to: Option, + ) -> Result; + async fn get_last_pings_for_servers( + &self, + server_ids: &[u32], + ) -> Result, RepositoryError>; // Servers async fn create_server(&self, server: DraftServer) -> Result; @@ -30,33 +38,70 @@ pub trait Repository: Send + Sync { // database: intended for public JSON endpoints. Never use their result // for write-backs (update_server/update_servers), the empty favicon // would erase the stored one. - async fn get_server_without_favicon(&self, server_id: u32) -> Result, RepositoryError>; + async fn get_server_without_favicon( + &self, + server_id: u32, + ) -> Result, RepositoryError>; async fn list_servers_without_favicon(&self) -> Result, RepositoryError>; - async fn get_servers_of_user_without_favicon(&self, user_id: String) -> Result, RepositoryError>; - async fn find_servers(&self, favicon_hash: Option<&str>, resolved_endpoint: Option<&str>, motd_hash: Option<&str>) -> Result, RepositoryError>; - async fn count_resolved_endpoints(&self, resolved_endpoint: &str, exclude_id: Option) -> Result; + async fn get_servers_of_user_without_favicon( + &self, + user_id: String, + ) -> Result, RepositoryError>; + async fn find_servers( + &self, + favicon_hash: Option<&str>, + resolved_endpoint: Option<&str>, + motd_hash: Option<&str>, + ) -> Result, RepositoryError>; + async fn count_resolved_endpoints( + &self, + resolved_endpoint: &str, + exclude_id: Option, + ) -> Result; // Alerts async fn create_alert(&self, alert: DraftAlert) -> Result; async fn delete_alert(&self, alert_id: u32, user_id: String) -> Result<(), RepositoryError>; async fn list_alerts_for_user(&self, user_id: String) -> Result, RepositoryError>; async fn list_alerts_for_server(&self, server_id: u32) -> Result, RepositoryError>; - async fn get_active_alerts_for_servers(&self, server_ids: &[u32]) -> Result, RepositoryError>; + async fn get_active_alerts_for_servers( + &self, + server_ids: &[u32], + ) -> Result, RepositoryError>; // Web Push Subscriptions - async fn create_subscription(&self, subscription: DraftWebPushSubscription) -> Result; - async fn delete_subscription(&self, endpoint: &str, user_id: &str) -> Result<(), RepositoryError>; + async fn create_subscription( + &self, + subscription: DraftWebPushSubscription, + ) -> Result; + async fn delete_subscription( + &self, + endpoint: &str, + user_id: &str, + ) -> Result<(), RepositoryError>; async fn delete_stale_subscription(&self, endpoint: &str) -> Result<(), RepositoryError>; - async fn get_subscriptions_for_users(&self, user_ids: &[String]) -> Result, RepositoryError>; - + async fn get_subscriptions_for_users( + &self, + user_ids: &[String], + ) -> Result, RepositoryError>; + async fn delete_server(&self, server_id: u32) -> Result<(), RepositoryError>; // Mojang API async fn get_mojang_api_status(&self) -> Result; async fn set_mojang_api_status(&self, is_down: bool) -> Result<(), RepositoryError>; - async fn start_mojang_api_downtime(&self, start_time: OffsetDateTime) -> Result<(), RepositoryError>; - async fn end_mojang_api_downtime(&self, end_time: OffsetDateTime) -> Result<(), RepositoryError>; - async fn get_mojang_api_downtimes(&self, limit: u32) -> Result, RepositoryError>; + async fn start_mojang_api_downtime( + &self, + start_time: OffsetDateTime, + ) -> Result<(), RepositoryError>; + async fn end_mojang_api_downtime( + &self, + end_time: OffsetDateTime, + ) -> Result<(), RepositoryError>; + async fn get_mojang_api_downtimes( + &self, + limit: u32, + ) -> Result, RepositoryError>; async fn initialize(&self) -> Result<(), RepositoryError>; } diff --git a/backend/rest-api/src/error.rs b/backend/rest-api/src/error.rs index 656205a..17b004b 100644 --- a/backend/rest-api/src/error.rs +++ b/backend/rest-api/src/error.rs @@ -1,11 +1,11 @@ -use std::fmt::{Debug}; -use axum::extract::rejection::{JsonRejection, PathRejection, QueryRejection}; use crate::response::ResponseFormat; +use axum::extract::rejection::{JsonRejection, PathRejection, QueryRejection}; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; +use repository::error::RepositoryError; use reqwest::Error; +use std::fmt::Debug; use thiserror::Error; -use repository::error::RepositoryError; #[derive(Error, Debug)] pub enum ServerCreationError { @@ -28,8 +28,9 @@ impl ServerCreationError { ServerCreationError::NotReachable => "error.server_creation.not_reachable", ServerCreationError::AlreadyExist => "error.server_creation.already_exist", ServerCreationError::DuplicationDetection(_) => "error.server_creation.duplicate", - ServerCreationError::Database => "error.server_creation.database_problem" - }.to_string() + ServerCreationError::Database => "error.server_creation.database_problem", + } + .to_string() } } @@ -58,10 +59,10 @@ pub enum AppError { #[error("{0}")] InvalidParam(#[from] PathRejection), - + #[error("{0}")] InvalidQuery(#[from] QueryRejection), - + #[error("{0}")] InvalidJson(#[from] JsonRejection), @@ -105,6 +106,11 @@ impl AppError { impl IntoResponse for AppError { fn into_response(self) -> Response { - ResponseFormat::<()>::error_translation(self.to_string(), self.translation_key(), self.status()).into_response() + ResponseFormat::<()>::error_translation( + self.to_string(), + self.translation_key(), + self.status(), + ) + .into_response() } } diff --git a/backend/rest-api/src/main.rs b/backend/rest-api/src/main.rs index c4e3b9c..48abe8d 100644 --- a/backend/rest-api/src/main.rs +++ b/backend/rest-api/src/main.rs @@ -1,33 +1,33 @@ pub mod error; -pub mod state; -pub mod routes; -pub mod response; pub mod middleware; +pub mod response; +pub mod routes; pub mod services; +pub mod state; pub mod utils; pub mod worker; -use services::clerk::account_checker::{fetch_clerk_jwks, JwksStore}; use crate::middleware::auth::auth_middleware; use crate::middleware::statistics::stats_middleware; use crate::routes::admin; use crate::state::AppState; use crate::utils::cache::TtlCache; -use axum::{extract::DefaultBodyLimit, http::Method}; -use axum::middleware::{from_fn, from_fn_with_state}; use axum::Router; -use tower_http::compression::CompressionLayer; -use std::env; -use std::net::SocketAddr; -use std::sync::Arc; +use axum::middleware::{from_fn, from_fn_with_state}; +use axum::{extract::DefaultBodyLimit, http::Method}; use dashmap::DashMap; use log::info; use minecraft_pinger::MinecraftPinger; +use repository::postgres::PostgresRepository; use reqwest::Client; +use services::clerk::account_checker::{JwksStore, fetch_clerk_jwks}; +use std::env; +use std::net::SocketAddr; +use std::sync::Arc; use tokio::net::TcpListener; use tokio::sync::RwLock; +use tower_http::compression::CompressionLayer; use tower_http::cors::{Any, CorsLayer}; -use repository::postgres::PostgresRepository; const DEFAULT_PORT: u16 = 3000; @@ -45,13 +45,11 @@ async fn main() { DEFAULT_PORT }); - // Init DB - let database_url = env::var("DATABASE_URL") - .unwrap_or_else(|_| { - info!("Please set the DATABASE_URL environment variable, using defaults."); - "postgres://anuser:password@localhost:5432/minecraft-stats".to_string() - }); + let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| { + info!("Please set the DATABASE_URL environment variable, using defaults."); + "postgres://anuser:password@localhost:5432/minecraft-stats".to_string() + }); let result = PostgresRepository::from_url(database_url).await; if let Err(err) = result { @@ -60,7 +58,6 @@ async fn main() { } let repository = result.unwrap(); - // Init clerk - Auth let clerk_instance = env::var("CLERK_URL"); if let Err(_) = clerk_instance { @@ -69,7 +66,8 @@ async fn main() { } let clerk_instance = clerk_instance.unwrap(); - let result = fetch_clerk_jwks(format!("{}/.well-known/jwks.json", clerk_instance).as_str()).await; + let result = + fetch_clerk_jwks(format!("{}/.well-known/jwks.json", clerk_instance).as_str()).await; if let Err(err) = result { info!("Error on clerk init: {}", err); return; @@ -77,15 +75,15 @@ async fn main() { let keys = result.unwrap(); let jwks_store = JwksStore::new(format!("{}/.well-known/jwks.json", clerk_instance), keys); - // Init clerk - API let clerk_secret_key = env::var("CLERK_SECRET_KEY"); if let Err(_) = clerk_secret_key { - info!("Please provide a CLERK_SECRET_KEY environment variable, some functionality may not work as expected."); + info!( + "Please provide a CLERK_SECRET_KEY environment variable, some functionality may not work as expected." + ); } let clerk_secret_key: Option = clerk_secret_key.ok().map(|s| s.into()); - // Init Pinger (for server add) let pinger = MinecraftPinger::new(); if let Err(err) = pinger { @@ -94,11 +92,16 @@ async fn main() { } let pinger = pinger.unwrap(); - // Init the main rest api let cors = CorsLayer::new() .allow_origin(Any) - .allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE, Method::PATCH]) + .allow_methods([ + Method::GET, + Method::POST, + Method::PUT, + Method::DELETE, + Method::PATCH, + ]) .allow_headers(Any); let body_limit = DefaultBodyLimit::max(1024 * 256); @@ -137,8 +140,15 @@ async fn main() { .layer(CompressionLayer::new()) .layer(body_limit); - let listener = TcpListener::bind(format!("0.0.0.0:{}", port)).await.unwrap(); + let listener = TcpListener::bind(format!("0.0.0.0:{}", port)) + .await + .unwrap(); info!("Listening on {}", listener.local_addr().unwrap()); - axum::serve(listener, app.into_make_service_with_connect_info::()).await.unwrap(); + axum::serve( + listener, + app.into_make_service_with_connect_info::(), + ) + .await + .unwrap(); } diff --git a/backend/rest-api/src/middleware/admin.rs b/backend/rest-api/src/middleware/admin.rs index 9699894..aa894ca 100644 --- a/backend/rest-api/src/middleware/admin.rs +++ b/backend/rest-api/src/middleware/admin.rs @@ -1,22 +1,26 @@ use axum::{ - body::Body, http::{Request, StatusCode}, middleware::Next, response::{IntoResponse, Response} + body::Body, + http::{Request, StatusCode}, + middleware::Next, + response::{IntoResponse, Response}, }; use crate::error::AppError; use crate::services::clerk::model::ClerkClaims; -pub async fn admin_middleware( - req: Request, - next: Next, -) -> Result { - let account = req.extensions().get::>().cloned().flatten(); +pub async fn admin_middleware(req: Request, next: Next) -> Result { + let account = req + .extensions() + .get::>() + .cloned() + .flatten(); let administrator = match account { Some(account) => account.is_admin.unwrap_or(false), None => false, }; if !administrator { - return Ok(AppError::Authentication.into_response()) + return Ok(AppError::Authentication.into_response()); } Ok(next.run(req).await) diff --git a/backend/rest-api/src/middleware/auth.rs b/backend/rest-api/src/middleware/auth.rs index 0db7f2a..497ce29 100644 --- a/backend/rest-api/src/middleware/auth.rs +++ b/backend/rest-api/src/middleware/auth.rs @@ -6,7 +6,7 @@ use axum::{ extract::State, http::{Request, StatusCode}, middleware::Next, - response::Response + response::Response, }; pub async fn auth_middleware( @@ -14,13 +14,16 @@ pub async fn auth_middleware( mut req: Request, next: Next, ) -> Result { - let auth_user = match req.headers().get(AUTHORIZATION) + let auth_user = match req + .headers() + .get(AUTHORIZATION) .and_then(|value| value.to_str().ok()) - .and_then(|token| token.strip_prefix("Bearer ")) { + .and_then(|token| token.strip_prefix("Bearer ")) + { Some(token) => verify_clerk_token(&state, token).await.ok(), None => None, }; req.extensions_mut().insert(auth_user); Ok(next.run(req).await) -} \ No newline at end of file +} diff --git a/backend/rest-api/src/middleware/mod.rs b/backend/rest-api/src/middleware/mod.rs index 6b17cd2..8914cb1 100644 --- a/backend/rest-api/src/middleware/mod.rs +++ b/backend/rest-api/src/middleware/mod.rs @@ -1,3 +1,3 @@ -pub mod auth; pub mod admin; -pub mod statistics; \ No newline at end of file +pub mod auth; +pub mod statistics; diff --git a/backend/rest-api/src/middleware/statistics.rs b/backend/rest-api/src/middleware/statistics.rs index b12c235..7e86131 100644 --- a/backend/rest-api/src/middleware/statistics.rs +++ b/backend/rest-api/src/middleware/statistics.rs @@ -1,7 +1,15 @@ +use axum::{ + body::{Body, HttpBody}, + extract::{ConnectInfo, Request}, + middleware::Next, + response::Response, +}; use std::{net::SocketAddr, time::Instant}; -use axum::{body::{Body, HttpBody}, extract::{ConnectInfo, Request}, middleware::Next, response::Response}; -use crate::{services::{clerk::model::ClerkClaims, statistics::loki::send_to_loki}, utils::real_ip::get_client_ip}; +use crate::{ + services::{clerk::model::ClerkClaims, statistics::loki::send_to_loki}, + utils::real_ip::get_client_ip, +}; pub async fn stats_middleware( ConnectInfo(addr): ConnectInfo, @@ -16,7 +24,11 @@ pub async fn stats_middleware( let ip = get_client_ip(request.headers(), &addr); // Request size - let request_headers_size: usize = request.headers().iter().map(|(k, v)| k.as_str().len() + v.len()).sum(); + let request_headers_size: usize = request + .headers() + .iter() + .map(|(k, v)| k.as_str().len() + v.len()) + .sum(); let request_body_size = request.body().size_hint().lower(); let total_request_size = (request_headers_size as u64) + request_body_size; @@ -35,7 +47,11 @@ pub async fn stats_middleware( let status = response.status().as_u16(); // Reponse size - let response_headers_size: usize = response.headers().iter().map(|(k, v)| k.as_str().len() + v.len()).sum(); + let response_headers_size: usize = response + .headers() + .iter() + .map(|(k, v)| k.as_str().len() + v.len()) + .sum(); let response_body_size = response.body().size_hint().lower(); let total_response_size = (response_headers_size as u64) + response_body_size; @@ -49,7 +65,8 @@ pub async fn stats_middleware( total_response_size, &ip, user_id, - ).await; + ) + .await; }); response diff --git a/backend/rest-api/src/response.rs b/backend/rest-api/src/response.rs index 5691a2c..ac654a5 100644 --- a/backend/rest-api/src/response.rs +++ b/backend/rest-api/src/response.rs @@ -1,6 +1,6 @@ +use axum::Json; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; -use axum::Json; use serde::Serialize; #[derive(Serialize, Debug)] diff --git a/backend/rest-api/src/routes/admin/mod.rs b/backend/rest-api/src/routes/admin/mod.rs index add8b72..698f8cc 100644 --- a/backend/rest-api/src/routes/admin/mod.rs +++ b/backend/rest-api/src/routes/admin/mod.rs @@ -1,3 +1,3 @@ -pub mod users; +pub mod routes; pub mod server; -pub mod routes; \ No newline at end of file +pub mod users; diff --git a/backend/rest-api/src/routes/admin/routes.rs b/backend/rest-api/src/routes/admin/routes.rs index 87279ca..cc07102 100644 --- a/backend/rest-api/src/routes/admin/routes.rs +++ b/backend/rest-api/src/routes/admin/routes.rs @@ -1,10 +1,14 @@ +use crate::utils::rate_limit::ClientIpKeyExtractor; +use crate::{ + middleware::admin::admin_middleware, + routes::admin::{server, users}, + state::AppState, +}; use axum::{Router, middleware::from_fn_with_state}; -use tower_governor::governor::GovernorConfigBuilder; use tower_governor::GovernorLayer; -use crate::{middleware::admin::admin_middleware, routes::admin::{server, users}, state::AppState}; -use crate::utils::rate_limit::ClientIpKeyExtractor; +use tower_governor::governor::GovernorConfigBuilder; -pub fn router(state: AppState) -> Router{ +pub fn router(state: AppState) -> Router { let admin_limit = GovernorConfigBuilder::default() .per_second(10) .burst_size(40) @@ -17,4 +21,4 @@ pub fn router(state: AppState) -> Router{ .merge(server::router()) .route_layer(from_fn_with_state(state.clone(), admin_middleware)) .route_layer(GovernorLayer::new(admin_limit)) -} \ No newline at end of file +} diff --git a/backend/rest-api/src/routes/admin/server.rs b/backend/rest-api/src/routes/admin/server.rs index 65883f4..918126a 100644 --- a/backend/rest-api/src/routes/admin/server.rs +++ b/backend/rest-api/src/routes/admin/server.rs @@ -1,15 +1,22 @@ -use std::time::Duration; +use crate::error::AppError::ServerNotFound; +use crate::{error::AppError, response::ResponseFormat, state::AppState}; use axum::routing::delete; -use axum::{extract::{rejection::{PathRejection, QueryRejection}, Path, Query, State}, routing::{post, patch}, Router, Json}; +use axum::{ + Json, Router, + extract::{ + Path, Query, State, + rejection::{PathRejection, QueryRejection}, + }, + routing::{patch, post}, +}; use minecraft_pinger::config::PingConfig; use minecraft_pinger::error::PingError; use minecraft_pinger::java::config::JavaPingConfig; -use reqwest::StatusCode; -use serde::Deserialize; use repository::duplicate_detection::DuplicateDetectionService; use repository::models::server::ServerType; -use crate::{error::AppError, response::ResponseFormat, state::AppState}; -use crate::error::AppError::ServerNotFound; +use reqwest::StatusCode; +use serde::Deserialize; +use std::time::Duration; pub fn router() -> Router { Router::new() @@ -22,16 +29,22 @@ pub fn router() -> Router { #[derive(Deserialize)] struct QueryParam { - hidden: bool + hidden: bool, } -async fn update_server_status(State(state): State, - id: Result, PathRejection>, - query: Result, QueryRejection>) -> Result, AppError> { +async fn update_server_status( + State(state): State, + id: Result, PathRejection>, + query: Result, QueryRejection>, +) -> Result, AppError> { let query = query?; let id = id?; - let mut server = state.repository.get_server(*id).await?.ok_or(ServerNotFound)?; + let mut server = state + .repository + .get_server(*id) + .await? + .ok_or(ServerNotFound)?; server.hidden = query.hidden; state.repository.update_server(&server).await?; @@ -39,8 +52,10 @@ async fn update_server_status(State(state): State, Ok(ResponseFormat::success((), StatusCode::OK)) } -async fn delete_server(State(state): State, - id: Result, PathRejection>) -> Result, AppError> { +async fn delete_server( + State(state): State, + id: Result, PathRejection>, +) -> Result, AppError> { let id = id?; state.repository.delete_server(*id).await?; Ok(ResponseFormat::success((), StatusCode::OK)) @@ -57,12 +72,17 @@ async fn update_server_favicon( Json(payload): Json, ) -> Result, AppError> { let id = id?; - let mut server = state.repository.get_server(*id).await?.ok_or(ServerNotFound)?; + let mut server = state + .repository + .get_server(*id) + .await? + .ok_or(ServerNotFound)?; server.last_favicon = payload.favicon; - + if server.last_favicon.is_some() { - server.favicon_hash = DuplicateDetectionService::hash_favicon(server.last_favicon.as_deref()); + server.favicon_hash = + DuplicateDetectionService::hash_favicon(server.last_favicon.as_deref()); server.forced_favicon = true; } else { server.favicon_hash = None; @@ -86,8 +106,8 @@ pub struct PingIpResponse { pub motd: Option, pub version: Option, pub favicon: Option, - pub current_players: Option, - pub max_players: Option, + pub current_players: Option, + pub max_players: Option, } async fn ping_server_ip( @@ -96,7 +116,11 @@ async fn ping_server_ip( Json(payload): Json, ) -> Result, AppError> { let id = id?; - let server = state.repository.get_server(*id).await?.ok_or(ServerNotFound)?; + let server = state + .repository + .get_server(*id) + .await? + .ok_or(ServerNotFound)?; let cfg = PingConfig::builder() .set_timeout(Duration::from_secs(2)) @@ -115,26 +139,32 @@ async fn ping_server_ip( for _ in 0..2 { let ping_res = match server.server_type { ServerType::Java => { - let res = state.pinger.ping_java_server(payload.ip.as_str(), payload.port, &java_cfg).await; + let res = state + .pinger + .ping_java_server(payload.ip.as_str(), payload.port, &java_cfg) + .await; if let Ok(ping) = &res { motd = serde_json::to_value(&ping.description).ok(); version = Some(ping.version.name.clone()); favicon = ping.favicon.clone(); current_players = Some(ping.players.online); - max_players = Some(ping.players.max as u32); + max_players = Some(ping.players.max); } if let Err(PingError::BlockedEndpoint(detail)) = &res { return Err(AppError::BlockedEndpoint(detail.clone())); } res.is_ok() - }, + } ServerType::Bedrock => { - let res = state.pinger.ping_bedrock_server(payload.ip.as_str(), payload.port, &cfg).await; + let res = state + .pinger + .ping_bedrock_server(payload.ip.as_str(), payload.port, &cfg) + .await; if let Ok(ping) = &res { motd = serde_json::to_value(&ping.motd).ok(); version = Some(ping.version.clone()); current_players = Some(ping.current_players); - max_players = Some(ping.max_players as u32); + max_players = Some(ping.max_players as i32); } if let Err(PingError::BlockedEndpoint(detail)) = &res { return Err(AppError::BlockedEndpoint(detail.clone())); @@ -149,14 +179,17 @@ async fn ping_server_ip( } } - Ok(ResponseFormat::success(PingIpResponse { - is_reachable, - motd, - version, - favicon, - current_players, - max_players - }, StatusCode::OK)) + Ok(ResponseFormat::success( + PingIpResponse { + is_reachable, + motd, + version, + favicon, + current_players, + max_players, + }, + StatusCode::OK, + )) } async fn update_server_ip( @@ -165,9 +198,14 @@ async fn update_server_ip( Json(payload): Json, ) -> Result, AppError> { let id = id?; - let mut server = state.repository.get_server(*id).await?.ok_or(ServerNotFound)?; + let mut server = state + .repository + .get_server(*id) + .await? + .ok_or(ServerNotFound)?; - let resolved_endpoint = DuplicateDetectionService::resolve_endpoint(payload.ip.as_str(), payload.port).await; + let resolved_endpoint = + DuplicateDetectionService::resolve_endpoint(payload.ip.as_str(), payload.port).await; server.ip = payload.ip; server.port = payload.port; @@ -176,4 +214,4 @@ async fn update_server_ip( state.repository.update_server(&server).await?; Ok(ResponseFormat::success((), StatusCode::OK)) -} \ No newline at end of file +} diff --git a/backend/rest-api/src/routes/admin/users.rs b/backend/rest-api/src/routes/admin/users.rs index fc7b2f0..c9a6420 100644 --- a/backend/rest-api/src/routes/admin/users.rs +++ b/backend/rest-api/src/routes/admin/users.rs @@ -1,19 +1,16 @@ -use axum::{extract::State, http::StatusCode, routing::get, Router}; +use axum::{Router, extract::State, http::StatusCode, routing::get}; -use crate::{ - error::AppError, - response::ResponseFormat, - state::AppState, -}; use crate::services::clerk::clerk_service; use crate::services::clerk::model::ClerkUser; +use crate::{error::AppError, response::ResponseFormat, state::AppState}; pub fn router() -> Router { - Router::new() - .route("/users", get(list_users)) + Router::new().route("/users", get(list_users)) } -async fn list_users(State(state): State) -> Result>, AppError> { +async fn list_users( + State(state): State, +) -> Result>, AppError> { let users = clerk_service::get_all_clerk_users(&state).await?; Ok(ResponseFormat::success(users, StatusCode::OK)) } diff --git a/backend/rest-api/src/routes/mod.rs b/backend/rest-api/src/routes/mod.rs index 8e2054a..9ca0ded 100644 --- a/backend/rest-api/src/routes/mod.rs +++ b/backend/rest-api/src/routes/mod.rs @@ -1,5 +1,5 @@ -pub mod record; -pub mod server; pub mod admin; -pub mod notification; pub mod mojang; +pub mod notification; +pub mod record; +pub mod server; diff --git a/backend/rest-api/src/routes/mojang/mod.rs b/backend/rest-api/src/routes/mojang/mod.rs index 1620719..96df2f3 100644 --- a/backend/rest-api/src/routes/mojang/mod.rs +++ b/backend/rest-api/src/routes/mojang/mod.rs @@ -1,2 +1,2 @@ -pub mod status; pub mod router; +pub mod status; diff --git a/backend/rest-api/src/routes/mojang/router.rs b/backend/rest-api/src/routes/mojang/router.rs index eaae642..ee9bb70 100644 --- a/backend/rest-api/src/routes/mojang/router.rs +++ b/backend/rest-api/src/routes/mojang/router.rs @@ -1,10 +1,10 @@ -use axum::routing::get; -use axum::Router; -use tower_governor::governor::GovernorConfigBuilder; -use tower_governor::GovernorLayer; +use super::status; use crate::state::AppState; use crate::utils::rate_limit::ClientIpKeyExtractor; -use super::status; +use axum::Router; +use axum::routing::get; +use tower_governor::GovernorLayer; +use tower_governor::governor::GovernorConfigBuilder; pub fn router() -> Router { let rate_limit_config = GovernorConfigBuilder::default() @@ -14,6 +14,8 @@ pub fn router() -> Router { .finish() .unwrap(); - Router::new() - .route("/status", get(status::get_status).route_layer(GovernorLayer::new(rate_limit_config))) + Router::new().route( + "/status", + get(status::get_status).route_layer(GovernorLayer::new(rate_limit_config)), + ) } diff --git a/backend/rest-api/src/routes/mojang/status.rs b/backend/rest-api/src/routes/mojang/status.rs index d418674..aeb5421 100644 --- a/backend/rest-api/src/routes/mojang/status.rs +++ b/backend/rest-api/src/routes/mojang/status.rs @@ -1,5 +1,5 @@ -use axum::{extract::State, Json}; -use crate::{state::AppState, error::AppError}; +use crate::{error::AppError, state::AppState}; +use axum::{Json, extract::State}; use repository::models::mojang::MojangApiDowntime; use serde::Serialize; @@ -9,9 +9,11 @@ pub struct MojangStatusResponse { pub downtimes: Vec, } -pub async fn get_status(State(state): State) -> Result, AppError> { +pub async fn get_status( + State(state): State, +) -> Result, AppError> { let is_down = state.repository.get_mojang_api_status().await?; let downtimes = state.repository.get_mojang_api_downtimes(50).await?; - + Ok(Json(MojangStatusResponse { is_down, downtimes })) } diff --git a/backend/rest-api/src/routes/notification.rs b/backend/rest-api/src/routes/notification.rs index d81aba1..70ca011 100644 --- a/backend/rest-api/src/routes/notification.rs +++ b/backend/rest-api/src/routes/notification.rs @@ -1,19 +1,19 @@ use crate::error::AppError; use crate::response::ResponseFormat; -use crate::state::AppState; use crate::services::clerk::model::ClerkClaims; +use crate::state::AppState; +use crate::utils::rate_limit::ClientIpKeyExtractor; use axum::extract::State; use axum::http::StatusCode; use axum::routing::{get, post}; use axum::{Extension, Json, Router}; -use repository::models::web_push::{WebPushSubscription, DraftWebPushSubscription}; +use repository::models::alert::Alert; +use repository::models::web_push::{DraftWebPushSubscription, WebPushSubscription}; use serde::{Deserialize, Serialize}; use std::env; use std::time::Duration; -use repository::models::alert::Alert; -use tower_governor::governor::GovernorConfigBuilder; use tower_governor::GovernorLayer; -use crate::utils::rate_limit::ClientIpKeyExtractor; +use tower_governor::governor::GovernorConfigBuilder; pub fn router() -> Router { let read_limit = GovernorConfigBuilder::default() @@ -34,9 +34,18 @@ pub fn router() -> Router { let write_layer = GovernorLayer::new(write_limit); Router::new() - .route("/vapid-key", get(get_vapid_key).route_layer(read_layer.clone())) - .route("/subscribe", post(subscribe_device).route_layer(write_layer.clone())) - .route("/unsubscribe", post(unsubscribe_device).route_layer(write_layer)) + .route( + "/vapid-key", + get(get_vapid_key).route_layer(read_layer.clone()), + ) + .route( + "/subscribe", + post(subscribe_device).route_layer(write_layer.clone()), + ) + .route( + "/unsubscribe", + post(unsubscribe_device).route_layer(write_layer), + ) .route("/list", get(list_alerts).route_layer(read_layer)) } @@ -46,7 +55,10 @@ async fn list_alerts( ) -> Result>, AppError> { let account = account.ok_or(AppError::Authentication)?; - let alerts = state.repository.list_alerts_for_user(account.id().clone()).await?; + let alerts = state + .repository + .list_alerts_for_user(account.id().clone()) + .await?; let user_alerts: Vec = alerts .into_iter() @@ -64,7 +76,10 @@ struct VapidKeyResponse { async fn get_vapid_key() -> Result, AppError> { let public_key = env::var("VAPID_PUBLIC_KEY") .unwrap_or_else(|_| "Please set VAPID_PUBLIC_KEY in env".to_string()); - Ok(ResponseFormat::success(VapidKeyResponse { public_key }, StatusCode::OK)) + Ok(ResponseFormat::success( + VapidKeyResponse { public_key }, + StatusCode::OK, + )) } #[derive(Deserialize)] @@ -104,6 +119,9 @@ async fn unsubscribe_device( ) -> Result, AppError> { let account = account.ok_or(AppError::Authentication)?; - state.repository.delete_subscription(&payload.endpoint, &account.sub).await?; + state + .repository + .delete_subscription(&payload.endpoint, &account.sub) + .await?; Ok(ResponseFormat::success((), StatusCode::NO_CONTENT)) } diff --git a/backend/rest-api/src/routes/record.rs b/backend/rest-api/src/routes/record.rs index 4497c29..cea08ee 100644 --- a/backend/rest-api/src/routes/record.rs +++ b/backend/rest-api/src/routes/record.rs @@ -1,21 +1,21 @@ -use crate::services::clerk::model::ClerkClaims; use crate::error::AppError; +use crate::response::ResponseFormat; +use crate::services::clerk::model::ClerkClaims; use crate::state::AppState; -use axum::extract::{Path, Query, State}; +use crate::utils::rate_limit::ClientIpKeyExtractor; use axum::extract::rejection::{PathRejection, QueryRejection}; -use axum::http::{StatusCode}; +use axum::extract::{Path, Query, State}; +use axum::http::StatusCode; +use axum::http::header::CONTENT_TYPE; +use axum::response::IntoResponse; use axum::routing::get; use axum::{Extension, Router}; -use axum::http::header::{CONTENT_TYPE}; -use axum::response::IntoResponse; use log::info; -use serde::{Deserialize}; -use time::{ OffsetDateTime}; +use serde::Deserialize; +use time::OffsetDateTime; use tokio::time::Instant; use tower_governor::GovernorLayer; use tower_governor::governor::GovernorConfigBuilder; -use crate::utils::rate_limit::ClientIpKeyExtractor; -use crate::response::ResponseFormat; pub fn router() -> Router { let rate_limit_config = GovernorConfigBuilder::default() @@ -25,8 +25,10 @@ pub fn router() -> Router { .finish() .unwrap(); - Router::new() - .route("/{id}", get(fetch_records).route_layer(GovernorLayer::new(rate_limit_config))) + Router::new().route( + "/{id}", + get(fetch_records).route_layer(GovernorLayer::new(rate_limit_config)), + ) } #[derive(Deserialize)] @@ -38,15 +40,20 @@ struct GetParam { pub json: Option, } -async fn fetch_records(State(state): State, - Extension(account): Extension>, - id: Result, PathRejection>, - query: Result, QueryRejection>) -> Result { +async fn fetch_records( + State(state): State, + Extension(account): Extension>, + id: Result, PathRejection>, + query: Result, QueryRejection>, +) -> Result { let id = *id?; let query = query?; let instant = Instant::now(); - let server = state.repository.get_server(id).await? + let server = state + .repository + .get_server(id) + .await? .ok_or(AppError::ServerNotFound)?; let is_admin = account.is_some_and(|u| u.is_admin()); @@ -55,14 +62,18 @@ async fn fetch_records(State(state): State, } let data = state.repository.get_pings(id, query.from, query.to).await?; - info!("Time to request all database data {}ms", instant.elapsed().as_millis()); + info!( + "Time to request all database data {}ms", + instant.elapsed().as_millis() + ); if !query.json.unwrap_or(false) { Ok(( StatusCode::OK, [(CONTENT_TYPE, "application/octet-stream")], - data.into_binary() - ).into_response()) + data.into_binary(), + ) + .into_response()) } else { Ok(ResponseFormat::success(data, StatusCode::OK).into_response()) } diff --git a/backend/rest-api/src/routes/server/create_alert.rs b/backend/rest-api/src/routes/server/create_alert.rs index 182d202..8c32644 100644 --- a/backend/rest-api/src/routes/server/create_alert.rs +++ b/backend/rest-api/src/routes/server/create_alert.rs @@ -1,12 +1,12 @@ -use axum::{Extension, Json}; -use axum::extract::{Path, State}; -use axum::http::StatusCode; -use serde::Deserialize; -use repository::models::alert::{Alert, AlertType, DraftAlert}; use crate::error::AppError; use crate::response::ResponseFormat; use crate::services::clerk::model::ClerkClaims; use crate::state::AppState; +use axum::extract::{Path, State}; +use axum::http::StatusCode; +use axum::{Extension, Json}; +use repository::models::alert::{Alert, AlertType, DraftAlert}; +use serde::Deserialize; #[derive(Deserialize)] pub(super) struct CreateAlertPayload { @@ -24,7 +24,11 @@ pub(super) async fn create_alert( let account = account.ok_or(AppError::Authentication)?; // Verify server exists - state.repository.get_server(server_id).await?.ok_or(AppError::ServerNotFound)?; + state + .repository + .get_server(server_id) + .await? + .ok_or(AppError::ServerNotFound)?; let draft = DraftAlert { user_id: account.sub.clone(), @@ -36,4 +40,4 @@ pub(super) async fn create_alert( let alert = state.repository.create_alert(draft).await?; Ok(ResponseFormat::success(alert, StatusCode::CREATED)) -} \ No newline at end of file +} diff --git a/backend/rest-api/src/routes/server/create_server.rs b/backend/rest-api/src/routes/server/create_server.rs index 4812c24..a901a68 100644 --- a/backend/rest-api/src/routes/server/create_server.rs +++ b/backend/rest-api/src/routes/server/create_server.rs @@ -1,27 +1,29 @@ -use std::time::Duration; -use axum::{Extension, Json}; -use axum::extract::rejection::JsonRejection; +use crate::error::{AppError, ServerCreationError}; +use crate::response::ResponseFormat; +use crate::services::clerk::model::ClerkClaims; +use crate::state::AppState; use axum::extract::State; +use axum::extract::rejection::JsonRejection; use axum::http::StatusCode; +use axum::{Extension, Json}; use log::{error, info}; use minecraft_pinger::config::PingConfig; use minecraft_pinger::error::PingError; use minecraft_pinger::java::config::JavaPingConfig; use repository::duplicate_detection::{DuplicateDetectionService, ServerFingerprint}; use repository::models::server::{DraftServer, Server, ServerType}; -use crate::error::{AppError, ServerCreationError}; -use crate::response::ResponseFormat; -use crate::services::clerk::model::ClerkClaims; -use crate::state::AppState; +use std::time::Duration; const ADD_TIMEOUT: Duration = Duration::from_secs(2); const PING_TRY_COUNT: usize = 2; -pub(super) async fn create_server(State(state): State, - Extension(account): Extension>, - query: Result, JsonRejection>) -> Result, AppError> { +pub(super) async fn create_server( + State(state): State, + Extension(account): Extension>, + query: Result, JsonRejection>, +) -> Result, AppError> { let account = account.ok_or(AppError::Authentication)?; - + let mut draft = query?.0; let (is_reachable, version_name) = ping_server(&state, &mut draft).await?; @@ -29,7 +31,8 @@ pub(super) async fn create_server(State(state): State, return Err(AppError::ServerCreation(ServerCreationError::NotReachable)); } - draft.resolved_endpoint = DuplicateDetectionService::resolve_endpoint(draft.ip.as_str(), draft.port).await; + draft.resolved_endpoint = + DuplicateDetectionService::resolve_endpoint(draft.ip.as_str(), draft.port).await; let fingerprint = ServerFingerprint { favicon_hash: draft.favicon_hash.clone(), @@ -38,11 +41,13 @@ pub(super) async fn create_server(State(state): State, version: version_name, }; - if let Some(duplicate) = DuplicateDetectionService::find_duplicate( - state.repository.as_ref(), - &fingerprint, - None, - ).await.map_err(|e| AppError::ServerCreation(ServerCreationError::DuplicationDetection(e.to_string())))? { + if let Some(duplicate) = + DuplicateDetectionService::find_duplicate(state.repository.as_ref(), &fingerprint, None) + .await + .map_err(|e| { + AppError::ServerCreation(ServerCreationError::DuplicationDetection(e.to_string())) + })? + { info!( "Server name {} is similar to existing server {} (ID: {}) with score {} (signals: {:?})", draft.name, @@ -67,7 +72,10 @@ pub(super) async fn create_server(State(state): State, Ok(ResponseFormat::success(rs, StatusCode::OK)) } -async fn ping_server(state: &AppState, draft: &mut DraftServer) -> Result<(bool, Option), AppError> { +async fn ping_server( + state: &AppState, + draft: &mut DraftServer, +) -> Result<(bool, Option), AppError> { let mut is_reachable = false; let mut version_name = None; @@ -81,9 +89,13 @@ async fn ping_server(state: &AppState, draft: &mut DraftServer) -> Result<(bool, for _ in 0..PING_TRY_COUNT { let ping_res = match draft.server_type { ServerType::Java => { - let res = state.pinger.ping_java_server(draft.ip.as_str(), draft.port, &java_cfg).await; + let res = state + .pinger + .ping_java_server(draft.ip.as_str(), draft.port, &java_cfg) + .await; if let Ok(ping) = &res { - draft.favicon_hash = DuplicateDetectionService::hash_favicon(ping.favicon.as_deref()); + draft.favicon_hash = + DuplicateDetectionService::hash_favicon(ping.favicon.as_deref()); let motd_value = serde_json::to_value(&ping.description).ok(); draft.motd_hash = DuplicateDetectionService::hash_motd(motd_value.as_ref()); version_name = Some(ping.version.name.clone()); @@ -96,9 +108,12 @@ async fn ping_server(state: &AppState, draft: &mut DraftServer) -> Result<(bool, } } res.is_ok() - }, + } ServerType::Bedrock => { - let res = state.pinger.ping_bedrock_server(draft.ip.as_str(), draft.port, &cfg).await; + let res = state + .pinger + .ping_bedrock_server(draft.ip.as_str(), draft.port, &cfg) + .await; if let Ok(ping) = &res { draft.favicon_hash = None; let motd_value = serde_json::to_value(&ping.motd).ok(); @@ -107,7 +122,10 @@ async fn ping_server(state: &AppState, draft: &mut DraftServer) -> Result<(bool, } if let Err(err) = &res { - error!("Could not add bedrock the server {} error {}", draft.ip, err); + error!( + "Could not add bedrock the server {} error {}", + draft.ip, err + ); if let PingError::BlockedEndpoint(detail) = err { return Err(AppError::BlockedEndpoint(detail.clone())); } @@ -123,4 +141,4 @@ async fn ping_server(state: &AppState, draft: &mut DraftServer) -> Result<(bool, } Ok((is_reachable, version_name)) -} \ No newline at end of file +} diff --git a/backend/rest-api/src/routes/server/delete_server.rs b/backend/rest-api/src/routes/server/delete_server.rs index 2eed2a6..911c92e 100644 --- a/backend/rest-api/src/routes/server/delete_server.rs +++ b/backend/rest-api/src/routes/server/delete_server.rs @@ -1,10 +1,10 @@ -use axum::Extension; -use axum::extract::{Path, State}; -use axum::http::StatusCode; use crate::error::AppError; use crate::response::ResponseFormat; use crate::services::clerk::model::ClerkClaims; use crate::state::AppState; +use axum::Extension; +use axum::extract::{Path, State}; +use axum::http::StatusCode; pub(super) async fn delete_alert( State(state): State, @@ -13,7 +13,10 @@ pub(super) async fn delete_alert( ) -> Result, AppError> { let account = account.ok_or(AppError::Authentication)?; - state.repository.delete_alert(alert_id, account.sub.clone()).await?; + state + .repository + .delete_alert(alert_id, account.sub.clone()) + .await?; Ok(ResponseFormat::success((), StatusCode::NO_CONTENT)) -} \ No newline at end of file +} diff --git a/backend/rest-api/src/routes/server/get_icon.rs b/backend/rest-api/src/routes/server/get_icon.rs index d3a46aa..67b7c24 100644 --- a/backend/rest-api/src/routes/server/get_icon.rs +++ b/backend/rest-api/src/routes/server/get_icon.rs @@ -1,32 +1,45 @@ +use crate::error::AppError; +use crate::state::AppState; use axum::extract::{Path, State}; use axum::http::header::{CACHE_CONTROL, CONTENT_TYPE}; use axum::response::IntoResponse; use base64::Engine; use base64::prelude::BASE64_STANDARD; -use crate::error::AppError; -use crate::state::AppState; const DEFAULT_ICON: &[u8] = include_bytes!("../../../assets/default_favicon.webp"); -const DEFAULT_ICON_CACHE: &str = "public, max-age=31536000, immutable"; +const DEFAULT_ICON_CACHE: &str = "public, max-age=86400"; pub(super) async fn get_server_icon( State(state): State, Path(id): Path, ) -> Result { - let server = state.repository.get_server(id).await? + let server = state + .repository + .get_server(id) + .await? .ok_or(AppError::ServerNotFound)?; - if let Some(favicon) = server.last_favicon && let Some(base64_data) = favicon.strip_prefix("data:image/png;base64,") { + if let Some(favicon) = server.last_favicon + && let Some(base64_data) = favicon.strip_prefix("data:image/png;base64,") + { if let Ok(image_bytes) = BASE64_STANDARD.decode(base64_data) { return Ok(( - [(CONTENT_TYPE, "image/png"), (CACHE_CONTROL, "public, max-age=86400")], - image_bytes - ).into_response()); + [ + (CONTENT_TYPE, "image/png"), + (CACHE_CONTROL, "public, max-age=86400"), + ], + image_bytes, + ) + .into_response()); } } Ok(( - [(CONTENT_TYPE, "image/webp"), (CACHE_CONTROL, DEFAULT_ICON_CACHE)], - DEFAULT_ICON - ).into_response()) -} \ No newline at end of file + [ + (CONTENT_TYPE, "image/webp"), + (CACHE_CONTROL, DEFAULT_ICON_CACHE), + ], + DEFAULT_ICON, + ) + .into_response()) +} diff --git a/backend/rest-api/src/routes/server/get_server.rs b/backend/rest-api/src/routes/server/get_server.rs index df9131a..82b7ba1 100644 --- a/backend/rest-api/src/routes/server/get_server.rs +++ b/backend/rest-api/src/routes/server/get_server.rs @@ -9,29 +9,42 @@ use axum::extract::rejection::PathRejection; use axum::extract::{Path, Query, State}; use axum::http::StatusCode; use futures::{StreamExt, stream}; -pub(super) async fn get_mine_server(State(state): State, - Query(query): Query, - Extension(account): Extension>) -> Result>, AppError> { +pub(super) async fn get_mine_server( + State(state): State, + Query(query): Query, + Extension(account): Extension>, +) -> Result>, AppError> { let account = account.ok_or(AppError::Authentication)?; let do_include_stats = query.include_stats.unwrap_or(false); - let result = state.repository.get_servers_of_user_without_favicon(account.id().clone()).await?; - let mut servers = stream::iter(result.into_iter() - .filter(|s| account.is_admin() || !s.hidden)) - .map(async |s| BiggerServerResponse::from(&state, s).await) - .buffered(5) - .collect() - .await; + let result = state + .repository + .get_servers_of_user_without_favicon(account.id().clone()) + .await?; + let mut servers = stream::iter( + result + .into_iter() + .filter(|s| account.is_admin() || !s.hidden), + ) + .map(async |s| BiggerServerResponse::from(&state, s).await) + .buffered(5) + .collect() + .await; include_stats(do_include_stats, &state, &mut servers).await?; Ok(ResponseFormat::success(servers, StatusCode::OK)) } -pub(super) async fn get_server(State(state): State, - Extension(account): Extension>, - id: Result, PathRejection>) -> Result, AppError> { - let server = state.repository.get_server_without_favicon(*id?).await? +pub(super) async fn get_server( + State(state): State, + Extension(account): Extension>, + id: Result, PathRejection>, +) -> Result, AppError> { + let server = state + .repository + .get_server_without_favicon(*id?) + .await? .ok_or(AppError::ServerNotFound)?; let is_admin = account.is_some_and(|u| u.is_admin()); @@ -43,5 +56,8 @@ pub(super) async fn get_server(State(state): State, .await .ok(); - Ok(ResponseFormat::success(BiggerServerResponse::from_user(&state, server, user).await, StatusCode::OK)) -} \ No newline at end of file + Ok(ResponseFormat::success( + BiggerServerResponse::from_user(&state, server, user).await, + StatusCode::OK, + )) +} diff --git a/backend/rest-api/src/routes/server/list_alert.rs b/backend/rest-api/src/routes/server/list_alert.rs index 365e64a..bac5369 100644 --- a/backend/rest-api/src/routes/server/list_alert.rs +++ b/backend/rest-api/src/routes/server/list_alert.rs @@ -1,11 +1,11 @@ -use axum::Extension; -use axum::extract::{Path, State}; -use axum::http::StatusCode; -use repository::models::alert::Alert; use crate::error::AppError; use crate::response::ResponseFormat; use crate::services::clerk::model::ClerkClaims; use crate::state::AppState; +use axum::Extension; +use axum::extract::{Path, State}; +use axum::http::StatusCode; +use repository::models::alert::Alert; pub(super) async fn list_alerts( State(state): State, @@ -15,7 +15,11 @@ pub(super) async fn list_alerts( let account = account.ok_or(AppError::Authentication)?; // Verify server exists - state.repository.get_server(server_id).await?.ok_or(AppError::ServerNotFound)?; + state + .repository + .get_server(server_id) + .await? + .ok_or(AppError::ServerNotFound)?; let alerts = state.repository.list_alerts_for_server(server_id).await?; @@ -25,4 +29,4 @@ pub(super) async fn list_alerts( .collect(); Ok(ResponseFormat::success(user_alerts, StatusCode::OK)) -} \ No newline at end of file +} diff --git a/backend/rest-api/src/routes/server/list_server.rs b/backend/rest-api/src/routes/server/list_server.rs index 0296eab..4fbc28e 100644 --- a/backend/rest-api/src/routes/server/list_server.rs +++ b/backend/rest-api/src/routes/server/list_server.rs @@ -1,40 +1,42 @@ -use std::sync::Arc; use crate::error::AppError; use crate::response::ResponseFormat; -use crate::routes::server::router::{include_stats, BiggerServerResponse, ServerListQueryParams}; +use crate::routes::server::router::{BiggerServerResponse, ServerListQueryParams, include_stats}; use crate::services::clerk::clerk_service::get_clerk_user_with_cache; use crate::services::clerk::model::{ClerkClaims, ClerkUser}; use crate::state::AppState; +use axum::Extension; use axum::extract::{Query, State}; use axum::http::StatusCode; -use axum::Extension; -use futures::{stream, StreamExt}; +use futures::{StreamExt, stream}; +use std::sync::Arc; -pub(super) async fn list_all_servers(State(state): State, - Query(query): Query, - Extension(user): Extension>) -> Result>, AppError> { +pub(super) async fn list_all_servers( + State(state): State, + Query(query): Query, + Extension(user): Extension>, +) -> Result>, AppError> { let do_include_stats = query.include_stats.unwrap_or(false); let server_list = state.repository.list_servers_without_favicon().await?; let is_admin = user.is_some_and(|u| u.is_admin()); - let mut servers: Vec = stream::iter(server_list - .into_iter() - .filter(|s| is_admin || !s.hidden)) - .map(async |server| { - let mut server_creator: Option> = None; - if query.include_owners.is_some_and(|t| t) { - server_creator = get_clerk_user_with_cache(&state, &server.user_id).await - .ok(); - } + let mut servers: Vec = + stream::iter(server_list.into_iter().filter(|s| is_admin || !s.hidden)) + .map(async |server| { + let mut server_creator: Option> = None; + if query.include_owners.is_some_and(|t| t) { + server_creator = get_clerk_user_with_cache(&state, &server.user_id) + .await + .ok(); + } - BiggerServerResponse::from_user(&state, server.into(), server_creator).await - }) - .buffered(5) - .collect() - .await; + BiggerServerResponse::from_user(&state, server.into(), server_creator).await + }) + .buffered(5) + .collect() + .await; include_stats(do_include_stats, &state, &mut servers).await?; Ok(ResponseFormat::success(servers, StatusCode::OK)) -} \ No newline at end of file +} diff --git a/backend/rest-api/src/routes/server/mod.rs b/backend/rest-api/src/routes/server/mod.rs index 275c665..26588cd 100644 --- a/backend/rest-api/src/routes/server/mod.rs +++ b/backend/rest-api/src/routes/server/mod.rs @@ -1,9 +1,9 @@ -pub mod router; -mod list_server; -mod get_server; -mod get_icon; mod create_alert; -mod list_alert; mod create_server; mod delete_server; -mod update_server; \ No newline at end of file +mod get_icon; +mod get_server; +mod list_alert; +mod list_server; +pub mod router; +mod update_server; diff --git a/backend/rest-api/src/routes/server/router.rs b/backend/rest-api/src/routes/server/router.rs index 63255b4..f5272c7 100644 --- a/backend/rest-api/src/routes/server/router.rs +++ b/backend/rest-api/src/routes/server/router.rs @@ -1,4 +1,3 @@ -use std::sync::Arc; use crate::error::AppError; use crate::routes::server::create_alert::create_alert; use crate::routes::server::create_server::create_server; @@ -9,17 +8,18 @@ use crate::routes::server::list_alert::list_alerts; use crate::routes::server::list_server::list_all_servers; use crate::routes::server::update_server::update_server_name; use crate::services::clerk::model::ClerkUser; +use crate::services::clients::{ClientInfos, get_client_infos}; use crate::state::AppState; -use axum::routing::{delete, get, patch, post}; +use crate::utils::rate_limit::ClientIpKeyExtractor; use axum::Router; +use axum::routing::{delete, get, patch, post}; use repository::models::record::RecordData; use repository::models::server::Server; use serde::{Deserialize, Serialize}; +use std::sync::Arc; use std::time::Duration; -use tower_governor::governor::GovernorConfigBuilder; use tower_governor::GovernorLayer; -use crate::services::clients::{get_client_infos, ClientInfos}; -use crate::utils::rate_limit::ClientIpKeyExtractor; +use tower_governor::governor::GovernorConfigBuilder; pub fn router() -> Router { let get_server_limit = GovernorConfigBuilder::default() @@ -68,26 +68,45 @@ pub fn router() -> Router { Router::new() .route("/", get(list_all_servers).route_layer(layer.clone())) - .route("/{id}", get(get_server).route_layer(layer.clone())) .route("/mine", get(get_mine_server).route_layer(layer.clone())) - .route("/{id}/icon", get(get_server_icon).route_layer(GovernorLayer::new(get_icon_limit))) - - .route("/{id}/alerts", get(list_alerts).route_layer(layer.clone()) - .post(create_alert).route_layer(GovernorLayer::new(push_alert_limit))) - .route("/alerts/{alert_id}", delete(delete_alert).route_layer(GovernorLayer::new(delete_alert_limit))) - - .route("/", post(create_server).route_layer(GovernorLayer::new(push_server_limit))) - .route("/{id}", patch(update_server_name).route_layer(GovernorLayer::new(patch_server_limit))) + .route( + "/{id}/icon", + get(get_server_icon).route_layer(GovernorLayer::new(get_icon_limit)), + ) + .route( + "/{id}/alerts", + get(list_alerts) + .route_layer(layer.clone()) + .post(create_alert) + .route_layer(GovernorLayer::new(push_alert_limit)), + ) + .route( + "/alerts/{alert_id}", + delete(delete_alert).route_layer(GovernorLayer::new(delete_alert_limit)), + ) + .route( + "/", + post(create_server).route_layer(GovernorLayer::new(push_server_limit)), + ) + .route( + "/{id}", + patch(update_server_name).route_layer(GovernorLayer::new(patch_server_limit)), + ) } -pub(super) async fn include_stats(include_stats: bool, - state: &AppState, - servers: &mut Vec) -> Result<(), AppError> { +pub(super) async fn include_stats( + include_stats: bool, + state: &AppState, + servers: &mut Vec, +) -> Result<(), AppError> { if include_stats { let server_ids: Vec = servers.iter().map(|s| s.server.id).collect(); - let mut records_map = state.repository.get_last_pings_for_servers(&server_ids).await?; + let mut records_map = state + .repository + .get_last_pings_for_servers(&server_ids) + .await?; for s in servers { s.data = records_map.remove(&s.server.id); } @@ -106,20 +125,22 @@ pub(super) struct ServerListQueryParams { pub(super) struct BiggerServerResponse { #[serde(flatten)] pub server: Server, - + #[serde(skip_serializing_if = "Option::is_none")] pub data: Option, - + #[serde(skip_serializing_if = "Option::is_none")] pub user: Option>, - - pub client_infos: ClientInfos + + pub client_infos: ClientInfos, } impl BiggerServerResponse { - pub async fn from_user(state: &AppState, - server: Server, - user: Option>) -> BiggerServerResponse { + pub async fn from_user( + state: &AppState, + server: Server, + user: Option>, + ) -> BiggerServerResponse { Self { client_infos: get_client_infos(state, &server).await, server, @@ -128,8 +149,7 @@ impl BiggerServerResponse { } } - pub async fn from(state: &AppState, - server: Server) -> BiggerServerResponse { + pub async fn from(state: &AppState, server: Server) -> BiggerServerResponse { Self { client_infos: get_client_infos(state, &server).await, server, @@ -137,4 +157,4 @@ impl BiggerServerResponse { user: None, } } -} \ No newline at end of file +} diff --git a/backend/rest-api/src/routes/server/update_server.rs b/backend/rest-api/src/routes/server/update_server.rs index 9f2c9b1..edb25ba 100644 --- a/backend/rest-api/src/routes/server/update_server.rs +++ b/backend/rest-api/src/routes/server/update_server.rs @@ -1,12 +1,12 @@ -use axum::{Extension, Json}; -use axum::extract::{Path, State}; -use axum::http::StatusCode; -use serde::Deserialize; -use repository::models::server::Server; use crate::error::AppError; use crate::response::ResponseFormat; use crate::services::clerk::model::ClerkClaims; use crate::state::AppState; +use axum::extract::{Path, State}; +use axum::http::StatusCode; +use axum::{Extension, Json}; +use repository::models::server::Server; +use serde::Deserialize; #[derive(Deserialize)] pub(super) struct UpdateServerPayload { @@ -21,7 +21,11 @@ pub(super) async fn update_server_name( ) -> Result, AppError> { let account = account.ok_or(AppError::Authentication)?; - let mut server = state.repository.get_server(id).await?.ok_or(AppError::ServerNotFound)?; + let mut server = state + .repository + .get_server(id) + .await? + .ok_or(AppError::ServerNotFound)?; let is_owner = server.user_id == account.sub; if !is_owner && !account.is_admin() { @@ -32,4 +36,4 @@ pub(super) async fn update_server_name( state.repository.update_server(&server).await?; Ok(ResponseFormat::success(server, StatusCode::OK)) -} \ No newline at end of file +} diff --git a/backend/rest-api/src/services/clerk/account_checker.rs b/backend/rest-api/src/services/clerk/account_checker.rs index e2531af..a848c23 100644 --- a/backend/rest-api/src/services/clerk/account_checker.rs +++ b/backend/rest-api/src/services/clerk/account_checker.rs @@ -1,8 +1,8 @@ use std::sync::{Arc, RwLock}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; -use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; -use rsa::{pkcs1v15::Pkcs1v15Sign, BoxedUint, RsaPublicKey}; +use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; +use rsa::{BoxedUint, RsaPublicKey, pkcs1v15::Pkcs1v15Sign}; use sha2::{Digest, Sha256}; use crate::services::clerk::model::ClerkClaims; @@ -72,7 +72,8 @@ impl JwksStore { slot.fetched_at = Instant::now(); slot.keys = fresh; - find_jwk(&slot.keys, kid).cloned() + find_jwk(&slot.keys, kid) + .cloned() .ok_or_else(|| "Pubkey not found".to_string()) } } @@ -81,12 +82,15 @@ fn find_jwk<'a>(jwks: &'a serde_json::Value, kid: &'a str) -> Option<&'a serde_j jwks.as_object() .and_then(|jwks| jwks.get("keys")) .and_then(|keys| keys.as_array()) - .and_then(|keys| keys.iter().find(|key| { - key.get("kid").and_then(|k| k.as_str()) == Some(kid) - })) + .and_then(|keys| { + keys.iter() + .find(|key| key.get("kid").and_then(|k| k.as_str()) == Some(kid)) + }) } -pub async fn fetch_clerk_jwks(jwks_url: &str) -> Result> { +pub async fn fetch_clerk_jwks( + jwks_url: &str, +) -> Result> { let response = reqwest::get(jwks_url) .await? .error_for_status()? @@ -101,21 +105,27 @@ pub async fn verify_clerk_token(state: &AppState, token: &str) -> Result(&URL_SAFE_NO_PAD.decode(header_b64).map_err(|e| e.to_string())?) - .map_err(|e| e.to_string())? - .get("kid").and_then(|k| k.as_str()).map(|s| s.to_string()) - .ok_or_else(|| "KID not found".to_string())?; + + let kid = serde_json::from_slice::( + &URL_SAFE_NO_PAD + .decode(header_b64) + .map_err(|e| e.to_string())?, + ) + .map_err(|e| e.to_string())? + .get("kid") + .and_then(|k| k.as_str()) + .map(|s| s.to_string()) + .ok_or_else(|| "KID not found".to_string())?; let jwk = state.jwks.resolve_jwk(&kid).await?; let n = jwk.get("n").and_then(|v| v.as_str()).ok_or("Missing n")?; let e = jwk.get("e").and_then(|v| v.as_str()).ok_or("Missing e")?; - + let n_bytes = URL_SAFE_NO_PAD.decode(n).map_err(|e| e.to_string())?; let e_bytes = URL_SAFE_NO_PAD.decode(e).map_err(|e| e.to_string())?; @@ -127,22 +137,28 @@ pub async fn verify_clerk_token(state: &AppState, token: &str) -> Result(); - rsa_key.verify(verifier, &hashed_content, &signature_bytes) + rsa_key + .verify(verifier, &hashed_content, &signature_bytes) .map_err(|e| format!("Verification error: {}", e))?; // Deserialize claims - let claims: ClerkClaims = serde_json::from_slice(&URL_SAFE_NO_PAD.decode(claims_b64).map_err(|e| e.to_string())?) - .map_err(|e| format!("Claims deserialization error: {}", e))?; + let claims: ClerkClaims = serde_json::from_slice( + &URL_SAFE_NO_PAD + .decode(claims_b64) + .map_err(|e| e.to_string())?, + ) + .map_err(|e| format!("Claims deserialization error: {}", e))?; validate_claims_time_window(&claims)?; diff --git a/backend/rest-api/src/services/clerk/clerk_service.rs b/backend/rest-api/src/services/clerk/clerk_service.rs index d08a1d6..e97e11b 100644 --- a/backend/rest-api/src/services/clerk/clerk_service.rs +++ b/backend/rest-api/src/services/clerk/clerk_service.rs @@ -1,13 +1,16 @@ -use std::{sync::Arc, time::Duration}; +use crate::services::clerk::model::ClerkUser; +use crate::{error::AppError, state::AppState}; use reqwest::Method; use serde_json::Value; -use crate::{error::AppError, state::AppState}; -use crate::services::clerk::model::ClerkUser; +use std::{sync::Arc, time::Duration}; const USER_CACHE_TTL: Duration = Duration::from_hours(2); const MAX_USERS_PER_PAGE: u64 = 500; -pub async fn get_clerk_user_with_cache(state: &AppState, user_id: &String) -> Result, AppError> { +pub async fn get_clerk_user_with_cache( + state: &AppState, + user_id: &String, +) -> Result, AppError> { let cached_user = state.user_cache.get(&user_id).await; if let Some(user) = cached_user { return Ok(user.clone()); @@ -16,10 +19,21 @@ pub async fn get_clerk_user_with_cache(state: &AppState, user_id: &String) -> Re get_clerk_user(state, user_id).await } -pub async fn get_clerk_user(state: &AppState, user_id: &String) -> Result, AppError> { - let token = state.clerk_secret_key.as_deref().ok_or(AppError::FeatureDisabled)?; +pub async fn get_clerk_user( + state: &AppState, + user_id: &String, +) -> Result, AppError> { + let token = state + .clerk_secret_key + .as_deref() + .ok_or(AppError::FeatureDisabled)?; - let user = state.http_client.request(Method::GET, format!("https://api.clerk.com/v1/users/{user_id}")) + let user = state + .http_client + .request( + Method::GET, + format!("https://api.clerk.com/v1/users/{user_id}"), + ) .bearer_auth(token) .send() .await? @@ -27,23 +41,33 @@ pub async fn get_clerk_user(state: &AppState, user_id: &String) -> Result Result, AppError> { - let token = state.clerk_secret_key.as_deref().ok_or(AppError::FeatureDisabled)?; + let token = state + .clerk_secret_key + .as_deref() + .ok_or(AppError::FeatureDisabled)?; - let user_count = state.http_client.request(Method::GET, "https://api.clerk.com/v1/users/count") + let user_count = state + .http_client + .request(Method::GET, "https://api.clerk.com/v1/users/count") .bearer_auth(token) .send() .await? .json::() .await?; - let user_count = user_count.get("total_count") - .ok_or(AppError::ClerkApiProblem("total_count not found".into()))?.as_u64() + let user_count = user_count + .get("total_count") + .ok_or(AppError::ClerkApiProblem("total_count not found".into()))? + .as_u64() .ok_or(AppError::ClerkApiProblem("total_count not u64".into()))?; let mut users: Vec = Vec::with_capacity(user_count as usize); @@ -53,7 +77,15 @@ pub async fn get_all_clerk_users(state: &AppState) -> Result, App let limit = &MAX_USERS_PER_PAGE; let offset = i * MAX_USERS_PER_PAGE; - let fetched_users = state.http_client.request(Method::GET,format!("https://api.clerk.com/v1/users?limit={}&offset={}", limit, offset)) + let fetched_users = state + .http_client + .request( + Method::GET, + format!( + "https://api.clerk.com/v1/users?limit={}&offset={}", + limit, offset + ), + ) .bearer_auth(token) .send() .await? diff --git a/backend/rest-api/src/services/clerk/mod.rs b/backend/rest-api/src/services/clerk/mod.rs index 2947056..483fd59 100644 --- a/backend/rest-api/src/services/clerk/mod.rs +++ b/backend/rest-api/src/services/clerk/mod.rs @@ -1,3 +1,3 @@ pub mod account_checker; -pub mod model; pub mod clerk_service; +pub mod model; diff --git a/backend/rest-api/src/services/clients/laby.rs b/backend/rest-api/src/services/clients/laby.rs index 7501162..7fbb814 100644 --- a/backend/rest-api/src/services/clients/laby.rs +++ b/backend/rest-api/src/services/clients/laby.rs @@ -1,17 +1,17 @@ -use std::collections::HashMap; -use std::sync::Arc; -use log::error; use crate::services::clients::BaseClientInfo; -use serde::{Deserialize, Serialize}; +use crate::state::AppState; +use log::error; use regex::Regex; use repository::models::server::Server; -use crate::state::AppState; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::sync::Arc; #[derive(Serialize)] pub struct LabyClientInfo { #[serde(flatten)] base: BaseClientInfo, - raw: Arc + raw: Arc, } impl RawLabyServer { @@ -34,15 +34,16 @@ impl RawLabyServer { fn construct_laby_info(state: &AppState, server: Arc) -> LabyClientInfo { let srv_part = state.laby_partner_cache.get(&server.direct_ip); - let is_partner = srv_part.map(|t| *t.value()) - .unwrap_or(false); + let is_partner = srv_part.map(|t| *t.value()).unwrap_or(false); let base = BaseClientInfo { partner: is_partner, - background: server.attachments.iter() + background: server + .attachments + .iter() .filter(|a| a.file_name.eq("background.webp") || a.file_name.eq("background.webp")) .map(|a| a.url.clone()) - .next() + .next(), }; LabyClientInfo { @@ -62,14 +63,15 @@ pub async fn get_laby_infos(state: &AppState, current_server: &Server) -> Option None } - #[derive(Deserialize)] struct LabyServersResponses { server_groups: HashMap, } pub async fn refresh_laby_infos(state: &AppState) { - let rs = state.http_client.get("https://laby.net/api/v3/serverGroups") + let rs = state + .http_client + .get("https://laby.net/api/v3/serverGroups") .send() .await; @@ -85,11 +87,16 @@ pub async fn refresh_laby_infos(state: &AppState) { } let mut guard = state.laby_servers_cache.write().await; - *guard = json.unwrap().server_groups.into_values() + *guard = json + .unwrap() + .server_groups + .into_values() .map(|mut server| { for wildcard in &server.wildcards { let mut regex_str = wildcard.replace(".", "\\."); - regex_str = regex_str.replace("%\\.", "(.*\\.)?").replace("*\\.", "(.*\\.)?"); + regex_str = regex_str + .replace("%\\.", "(.*\\.)?") + .replace("*\\.", "(.*\\.)?"); regex_str = regex_str.replace("%", ".*").replace("*", ".*"); let final_regex = format!("(?i)^{}$", regex_str); @@ -113,7 +120,9 @@ pub struct ServerInfo { } pub async fn refresh_laby_partner_infos(state: &AppState) { - let rs = state.http_client.get("https://laby.net/api/v3/publicServers") + let rs = state + .http_client + .get("https://laby.net/api/v3/publicServers") .send() .await; @@ -210,4 +219,4 @@ pub struct Location { pub city: Option, pub country: String, pub country_code: Option, -} \ No newline at end of file +} diff --git a/backend/rest-api/src/services/clients/lunar.rs b/backend/rest-api/src/services/clients/lunar.rs index 39341c7..e25d6ce 100644 --- a/backend/rest-api/src/services/clients/lunar.rs +++ b/backend/rest-api/src/services/clients/lunar.rs @@ -1,10 +1,10 @@ -use std::collections::HashMap; -use std::sync::Arc; -use log::{error}; -use serde::{Deserialize, Serialize}; -use repository::models::server::Server; use crate::services::clients::BaseClientInfo; use crate::state::AppState; +use log::error; +use repository::models::server::Server; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::sync::Arc; #[derive(Serialize)] pub struct LunarClientInfo { @@ -16,26 +16,31 @@ pub struct LunarClientInfo { fn construct_lunar_info(state: &AppState, server: Arc) -> LunarClientInfo { let srv_part = state.lunar_partner_cache.get(&server.id); - let is_partner = srv_part.as_ref() + let is_partner = srv_part + .as_ref() .and_then(|info| info.partnered) .unwrap_or(false); let base = BaseClientInfo { partner: is_partner, - background: server.images.background.clone() + background: server.images.background.clone(), }; LunarClientInfo { base, raw: server.clone(), - raw_partner: srv_part.map(|info| info.value().clone()) + raw_partner: srv_part.map(|info| info.value().clone()), } } pub async fn get_lunar_infos(state: &AppState, current_server: &Server) -> Option { let guard = state.lunar_servers_cache.read().await; for server in guard.iter() { - if server.addresses.contains(¤t_server.ip) || server.primary_address.eq_ignore_ascii_case(current_server.ip.as_str()) { + if server.addresses.contains(¤t_server.ip) + || server + .primary_address + .eq_ignore_ascii_case(current_server.ip.as_str()) + { return Some(construct_lunar_info(state, server.clone())); } } @@ -43,9 +48,10 @@ pub async fn get_lunar_infos(state: &AppState, current_server: &Server) -> Optio None } - pub async fn refresh_lunar_infos(state: &AppState) { - let rs = state.http_client.get("https://servermappings.lunarclientcdn.com/servers.json") + let rs = state + .http_client + .get("https://servermappings.lunarclientcdn.com/servers.json") .send() .await; @@ -61,9 +67,7 @@ pub async fn refresh_lunar_infos(state: &AppState) { } let mut guard = state.lunar_servers_cache.write().await; - *guard = json.unwrap().into_iter() - .map(Arc::new) - .collect(); + *guard = json.unwrap().into_iter().map(Arc::new).collect(); } #[derive(Deserialize)] @@ -92,7 +96,6 @@ pub async fn refresh_lunar_partner_infos(state: &AppState) { } } - #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RawLunarPartnerServer { @@ -218,4 +221,4 @@ pub struct TebexStore { pub struct TebexBackground { pub lower_color: String, pub upper_color: String, -} \ No newline at end of file +} diff --git a/backend/rest-api/src/services/clients/mod.rs b/backend/rest-api/src/services/clients/mod.rs index 6e0013e..010161e 100644 --- a/backend/rest-api/src/services/clients/mod.rs +++ b/backend/rest-api/src/services/clients/mod.rs @@ -1,11 +1,11 @@ -use crate::services::clients::laby::{get_laby_infos, LabyClientInfo}; +use crate::services::clients::laby::{LabyClientInfo, get_laby_infos}; use crate::services::clients::lunar::{LunarClientInfo, get_lunar_infos}; use crate::state::AppState; use repository::models::server::Server; use serde::Serialize; -pub mod lunar; pub mod laby; +pub mod lunar; #[derive(Serialize)] pub struct ClientInfos { @@ -19,12 +19,12 @@ pub struct ClientInfos { pub struct BaseClientInfo { pub partner: bool, #[serde(skip_serializing_if = "Option::is_none")] - pub background: Option + pub background: Option, } pub async fn get_client_infos(state: &AppState, server: &Server) -> ClientInfos { ClientInfos { laby: get_laby_infos(state, server).await, - lunar: get_lunar_infos(state, server).await + lunar: get_lunar_infos(state, server).await, } -} \ No newline at end of file +} diff --git a/backend/rest-api/src/services/mod.rs b/backend/rest-api/src/services/mod.rs index e04b0b1..8503cad 100644 --- a/backend/rest-api/src/services/mod.rs +++ b/backend/rest-api/src/services/mod.rs @@ -1,3 +1,3 @@ pub mod clerk; +pub mod clients; pub mod statistics; -pub mod clients; \ No newline at end of file diff --git a/backend/rest-api/src/services/statistics/loki.rs b/backend/rest-api/src/services/statistics/loki.rs index 17c0c23..f8acd86 100644 --- a/backend/rest-api/src/services/statistics/loki.rs +++ b/backend/rest-api/src/services/statistics/loki.rs @@ -1,6 +1,6 @@ +use serde::Serialize; use std::collections::HashMap; use std::env; -use serde::Serialize; use time::OffsetDateTime; #[derive(Serialize)] @@ -14,13 +14,16 @@ struct LokiStream { values: Vec<[String; 2]>, // [Timestamp_Nano, Log_Message] } -pub async fn send_to_loki(route: &str, - method: &str, - status: u16, - duration: u128, - request_size: u64, - reponse_size: u64, - ip: &str, user_id: Option) { +pub async fn send_to_loki( + route: &str, + method: &str, + status: u16, + duration: u128, + request_size: u64, + reponse_size: u64, + ip: &str, + user_id: Option, +) { let result = env::var("LOKI_URL"); if let Ok(result) = result { let log_content = serde_json::json!({ @@ -32,7 +35,8 @@ pub async fn send_to_loki(route: &str, "duration_ms": duration, "ip": ip, "user_id": user_id - }).to_string(); + }) + .to_string(); let nanoseconds = OffsetDateTime::now_utc().unix_timestamp_nanos().to_string(); @@ -48,10 +52,7 @@ pub async fn send_to_loki(route: &str, tokio::spawn(async move { let client = reqwest::Client::new(); - let _ = client.post(result) - .json(&payload) - .send() - .await; + let _ = client.post(result).json(&payload).send().await; }); } } diff --git a/backend/rest-api/src/state.rs b/backend/rest-api/src/state.rs index 7819557..9146f96 100644 --- a/backend/rest-api/src/state.rs +++ b/backend/rest-api/src/state.rs @@ -15,7 +15,7 @@ use crate::utils::cache::TtlCache; pub struct AppState { pub repository: Arc, pub pinger: MinecraftPinger, - + pub http_client: Client, pub jwks: JwksStore, diff --git a/backend/rest-api/src/utils/mod.rs b/backend/rest-api/src/utils/mod.rs index 5fddef7..85c4c90 100644 --- a/backend/rest-api/src/utils/mod.rs +++ b/backend/rest-api/src/utils/mod.rs @@ -1,3 +1,3 @@ pub mod cache; -pub mod real_ip; pub mod rate_limit; +pub mod real_ip; diff --git a/backend/rest-api/src/utils/real_ip.rs b/backend/rest-api/src/utils/real_ip.rs index 5143030..eb08c25 100644 --- a/backend/rest-api/src/utils/real_ip.rs +++ b/backend/rest-api/src/utils/real_ip.rs @@ -32,6 +32,5 @@ pub fn get_client_ip(headers: &HeaderMap, fallback_addr: &SocketAddr) -> String } } - fallback_addr.ip().to_string() } diff --git a/backend/rest-api/src/worker/client_fetcher.rs b/backend/rest-api/src/worker/client_fetcher.rs index 3cd5204..63adc0f 100644 --- a/backend/rest-api/src/worker/client_fetcher.rs +++ b/backend/rest-api/src/worker/client_fetcher.rs @@ -1,8 +1,8 @@ +use crate::services::clients::laby::{refresh_laby_infos, refresh_laby_partner_infos}; use crate::services::clients::lunar::{refresh_lunar_infos, refresh_lunar_partner_infos}; use crate::state::AppState; use std::time::Duration; use tokio::time::sleep; -use crate::services::clients::laby::{refresh_laby_infos, refresh_laby_partner_infos}; const FETCH_DELAY: Duration = Duration::from_hours(1); @@ -18,4 +18,4 @@ pub fn start(state: AppState) { sleep(FETCH_DELAY).await; } }); -} \ No newline at end of file +} diff --git a/backend/rest-api/src/worker/mod.rs b/backend/rest-api/src/worker/mod.rs index 9663542..cc7dbe8 100644 --- a/backend/rest-api/src/worker/mod.rs +++ b/backend/rest-api/src/worker/mod.rs @@ -1 +1 @@ -pub mod client_fetcher; \ No newline at end of file +pub mod client_fetcher; diff --git a/backend/worker/src/main.rs b/backend/worker/src/main.rs index 60c90bc..96d4e17 100644 --- a/backend/worker/src/main.rs +++ b/backend/worker/src/main.rs @@ -1,13 +1,13 @@ -use std::env; +use crate::tasks::communication::{VerifierToSender, WorkerToVerifier}; +use crate::tasks::mojang_checker::mojang_checker_worker; +use crate::tasks::pinger::ping_worker; +use crate::tasks::sender::sender_worker; +use crate::tasks::verifier::verifier_worker; +use log::info; use repository::postgres::PostgresRepository; +use std::env; use std::time::Duration; -use log::info; use tokio::sync::mpsc; -use crate::tasks::communication::{WorkerToVerifier, VerifierToSender}; -use crate::tasks::pinger::ping_worker; -use crate::tasks::verifier::verifier_worker; -use crate::tasks::sender::sender_worker; -use crate::tasks::mojang_checker::mojang_checker_worker; mod tasks; @@ -22,8 +22,9 @@ async fn main() { info!("Starting server"); - let database_url = env::var("DATABASE_URL") - .unwrap_or_else(|_| "postgres://anuser:password@localhost:5432/minecraft-stats".to_string()); + let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| { + "postgres://anuser:password@localhost:5432/minecraft-stats".to_string() + }); let repository = PostgresRepository::from_url(database_url).await.unwrap(); @@ -36,7 +37,7 @@ async fn main() { // Spawn Pinger let repository_worker = repository.clone(); tokio::spawn(async move { - ping_worker(repository_worker, tx_verifier).await; + ping_worker(repository_worker, tx_verifier).await; }); // Spawn Verifier diff --git a/backend/worker/src/tasks/communication.rs b/backend/worker/src/tasks/communication.rs index 0fd4dfe..1ec64a0 100644 --- a/backend/worker/src/tasks/communication.rs +++ b/backend/worker/src/tasks/communication.rs @@ -2,10 +2,11 @@ use repository::models::alert::Alert; use repository::models::server::ServerStatus::Offline; use repository::models::server::{Server, ServerStatus}; -pub const MC_DEFAULT_ICON: &str = "https://wd40.theking90000.be/files/ee292f4a-dfff-4c5f-b65e-1beca56ec24f"; +pub const MC_DEFAULT_ICON: &str = + "https://wd40.theking90000.be/files/ee292f4a-dfff-4c5f-b65e-1beca56ec24f"; pub enum WorkerToVerifier { - ServerStatusUpdated(ServerStateChange) + ServerStatusUpdated(ServerStateChange), } #[derive(Clone)] @@ -14,10 +15,10 @@ pub struct ServerStateChange { pub name: String, pub old_status: Option, - pub old_players: Option, + pub old_players: Option, pub new_status: ServerStatus, - pub new_players: Option, + pub new_players: Option, } impl ServerStateChange { @@ -38,7 +39,7 @@ impl From<&Server> for ServerStateChange { // Two values changed before server update new_status: Offline, - new_players: None + new_players: None, } } } @@ -53,16 +54,20 @@ pub struct TriggeredAlertNotification { pub server_name: String, pub _old_status: Option, - pub _old_players: Option, + pub _old_players: Option, pub _new_status: ServerStatus, - pub new_players: Option, + pub new_players: Option, } impl TriggeredAlertNotification { pub fn get_logo(&self) -> String { if let Ok(api_base) = std::env::var("API_BASE_URL") { - return format!("{}/servers/{}/icon", api_base.trim_end_matches('/'), self.alert.server_id); + return format!( + "{}/servers/{}/icon", + api_base.trim_end_matches('/'), + self.alert.server_id + ); } MC_DEFAULT_ICON.to_string() } @@ -80,4 +85,4 @@ impl TriggeredAlertNotification { new_players: state.new_players, } } -} \ No newline at end of file +} diff --git a/backend/worker/src/tasks/mod.rs b/backend/worker/src/tasks/mod.rs index 5211328..54f05d9 100644 --- a/backend/worker/src/tasks/mod.rs +++ b/backend/worker/src/tasks/mod.rs @@ -1,5 +1,5 @@ -pub mod pinger; -pub mod communication; -pub mod verifier; -pub mod sender; -pub mod mojang_checker; \ No newline at end of file +pub mod communication; +pub mod mojang_checker; +pub mod pinger; +pub mod sender; +pub mod verifier; diff --git a/backend/worker/src/tasks/mojang_checker.rs b/backend/worker/src/tasks/mojang_checker.rs index 6f1455f..d101070 100644 --- a/backend/worker/src/tasks/mojang_checker.rs +++ b/backend/worker/src/tasks/mojang_checker.rs @@ -1,10 +1,10 @@ use crate::DELAY_BETWEEN_EACH_PING; +use log::{error, info, warn}; +use openssl::rand::rand_bytes; +use openssl::sha::Sha1; use repository::repository::Repository; use std::time::{Duration, Instant}; -use log::{info, error, warn}; use time::OffsetDateTime; -use openssl::sha::Sha1; -use openssl::rand::rand_bytes; use tokio::time::sleep; const REQUIRED_FAILURES: u8 = 3; @@ -17,7 +17,7 @@ fn generate_minecraft_hash(server_id: &str, shared_secret: &[u8], public_key: &[ let digest = hasher.finish(); let is_negative = (digest[0] & 0x80) != 0; - + let hex_str = if is_negative { let mut carry = true; let mut inverted = [0u8; 20]; @@ -34,7 +34,7 @@ fn generate_minecraft_hash(server_id: &str, shared_secret: &[u8], public_key: &[ } else { hex::encode(digest).trim_start_matches('0').to_string() }; - + if hex_str.is_empty() || hex_str == "-" { "0".to_string() } else { @@ -75,7 +75,7 @@ pub async fn mojang_checker_worker(repository: impl Repository + Clone) { warn!("Mojang API returned server error: {}", status); is_currently_down = true; } - }, + } Err(e) => { warn!("Failed to reach Mojang API: {}", e); is_currently_down = true; @@ -93,13 +93,20 @@ pub async fn mojang_checker_worker(repository: impl Repository + Clone) { match repository.get_mojang_api_status().await { Ok(db_is_down) => { if is_confirmed_down && !db_is_down { - info!("Mojang Session API went DOWN ({} consecutive failures)", consecutive_failures); + info!( + "Mojang Session API went DOWN ({} consecutive failures)", + consecutive_failures + ); let _ = repository.set_mojang_api_status(true).await; - let _ = repository.start_mojang_api_downtime(OffsetDateTime::now_utc()).await; + let _ = repository + .start_mojang_api_downtime(OffsetDateTime::now_utc()) + .await; } else if !is_currently_down && db_is_down { info!("Mojang Session API is back UP"); let _ = repository.set_mojang_api_status(false).await; - let _ = repository.end_mojang_api_downtime(OffsetDateTime::now_utc()).await; + let _ = repository + .end_mojang_api_downtime(OffsetDateTime::now_utc()) + .await; } } Err(e) => error!("Failed to get Mojang API status from DB: {}", e), diff --git a/backend/worker/src/tasks/pinger.rs b/backend/worker/src/tasks/pinger.rs index 2d7003b..bbf3e0d 100644 --- a/backend/worker/src/tasks/pinger.rs +++ b/backend/worker/src/tasks/pinger.rs @@ -1,10 +1,12 @@ use crate::tasks::communication::{ServerStateChange, WorkerToVerifier}; use crate::{DELAY_BETWEEN_EACH_PING, MAX_CONCURRENT_PING, MAX_PING_RESPONSE_TIME, PING_TRY_COUNT}; -use futures::{stream, StreamExt}; -use minecraft_pinger::config::PingConfig; +use futures::{StreamExt, stream}; +use log::info; +use minecraft_pinger::MinecraftPinger; use minecraft_pinger::bedrock::model::BedrockPing; +use minecraft_pinger::config::PingConfig; +use minecraft_pinger::java::config::JavaPingConfig; use minecraft_pinger::java::model::JavaPing; -use minecraft_pinger::MinecraftPinger; use repository::duplicate_detection::DuplicateDetectionService; use repository::models::record::Record; use repository::models::server::{Server, ServerStatus, ServerType}; @@ -12,15 +14,13 @@ use repository::postgres::PostgresRepository; use repository::repository::Repository; use std::sync::Arc; use std::time::{Duration, Instant}; -use log::info; -use minecraft_pinger::java::config::JavaPingConfig; use time::OffsetDateTime; use tokio::sync::mpsc::Sender; use tokio::time::sleep; enum PingResultType { Java(JavaPing), - Bedrock(BedrockPing) + Bedrock(BedrockPing), } async fn update_server_from_ping(server: &mut Server, ping: PingResultType) { @@ -35,10 +35,13 @@ async fn update_server_from_ping(server: &mut Server, ping: PingResultType) { if let Some(players) = ping.players.sample { if !players.is_empty() { - server.last_sample = Some(players.iter() - .map(|p| p.name.as_str()) - .collect::>() - .join("\n")); + server.last_sample = Some( + players + .iter() + .map(|p| p.name.as_str()) + .collect::>() + .join("\n"), + ); } else { server.last_sample = None; } @@ -52,14 +55,17 @@ async fn update_server_from_ping(server: &mut Server, ping: PingResultType) { } if !server.forced_favicon && server.last_favicon != ping.favicon { - server.favicon_hash = DuplicateDetectionService::hash_favicon(ping.favicon.as_deref()); + server.favicon_hash = + DuplicateDetectionService::hash_favicon(ping.favicon.as_deref()); server.last_favicon = ping.favicon; } if let Some(addr) = ping.connected_addr { server.resolved_endpoint = Some(addr.to_string()); } else if server.resolved_endpoint.is_none() { - server.resolved_endpoint = DuplicateDetectionService::resolve_endpoint(server.ip.as_str(), server.port).await; + server.resolved_endpoint = + DuplicateDetectionService::resolve_endpoint(server.ip.as_str(), server.port) + .await; } } PingResultType::Bedrock(ping) => { @@ -85,7 +91,9 @@ async fn update_server_from_ping(server: &mut Server, ping: PingResultType) { if let Some(addr) = ping.connected_addr { server.resolved_endpoint = Some(addr.to_string()); } else if server.resolved_endpoint.is_none() { - server.resolved_endpoint = DuplicateDetectionService::resolve_endpoint(server.ip.as_str(), server.port).await; + server.resolved_endpoint = + DuplicateDetectionService::resolve_endpoint(server.ip.as_str(), server.port) + .await; } } } @@ -99,10 +107,12 @@ pub async fn ping_worker(repository: PostgresRepository, state_updater: Sender { - let ping_rs = pinger.ping_java_server(server.ip.as_str(), server.port, java_pinger_config.as_ref()).await; + let ping_rs = pinger + .ping_java_server( + server.ip.as_str(), + server.port, + java_pinger_config.as_ref(), + ) + .await; match ping_rs { Ok(ping) => { players_online = ping.players.online; - update_server_from_ping(&mut server, PingResultType::Java(ping)).await; + update_server_from_ping( + &mut server, + PingResultType::Java(ping), + ) + .await; is_ok = true; - }, + } Err(e) => { err_msg = Some(format!("{}", e)); } } - }, + } ServerType::Bedrock => { - let ping_rs = pinger.ping_bedrock_server(server.ip.as_str(), server.port, pinger_config.as_ref()).await; + let ping_rs = pinger + .ping_bedrock_server( + server.ip.as_str(), + server.port, + pinger_config.as_ref(), + ) + .await; match ping_rs { Ok(ping) => { players_online = ping.current_players; - update_server_from_ping(&mut server, PingResultType::Bedrock(ping)).await; + update_server_from_ping( + &mut server, + PingResultType::Bedrock(ping), + ) + .await; is_ok = true; - }, + } Err(e) => { err_msg = Some(format!("{}", e)); } @@ -169,7 +199,9 @@ pub async fn ping_worker(repository: PostgresRepository, state_updater: Sender, -) { +pub async fn sender_worker(repository: PostgresRepository, mut rx: Receiver) { info!("Sender task started"); let private_key_pem = env::var("VAPID_PRIVATE_KEY").ok(); @@ -84,7 +81,10 @@ pub async fn sender_worker( // Group subscriptions by user_id let mut subs_by_user = HashMap::new(); for sub in subscriptions { - subs_by_user.entry(sub.user_id.clone()).or_insert_with(Vec::new).push(sub); + subs_by_user + .entry(sub.user_id.clone()) + .or_insert_with(Vec::new) + .push(sub); } for notification in notifications { @@ -94,17 +94,29 @@ pub async fn sender_worker( let message_title = format!("Alerte Serveur: {}", notification.server_name); let message_body = match notification.alert.alert_type { - AlertType::StatusToOffline => format!("Le serveur {} vient de passer HORS-LIGNE !", notification.server_name), - AlertType::StatusToOnline => format!("Le serveur {} est de nouveau EN LIGNE !", notification.server_name), + AlertType::StatusToOffline => format!( + "Le serveur {} vient de passer HORS-LIGNE !", + notification.server_name + ), + AlertType::StatusToOnline => format!( + "Le serveur {} est de nouveau EN LIGNE !", + notification.server_name + ), AlertType::PlayerAbove => { let current = notification.new_players.unwrap_or(0); let threshold = notification.alert.player_threshold.unwrap_or(0); - format!("Le nombre de joueurs sur {} a dépassé {} (Actuel : {}) !", notification.server_name, threshold, current) + format!( + "Le nombre de joueurs sur {} a dépassé {} (Actuel : {}) !", + notification.server_name, threshold, current + ) } AlertType::PlayerBelow => { let current = notification.new_players.unwrap_or(0); let threshold = notification.alert.player_threshold.unwrap_or(0); - format!("Le nombre de joueurs sur {} est descendu en-dessous de {} (Actuel : {}) !", notification.server_name, threshold, current) + format!( + "Le nombre de joueurs sur {} est descendu en-dessous de {} (Actuel : {}) !", + notification.server_name, threshold, current + ) } }; @@ -120,23 +132,28 @@ pub async fn sender_worker( for sub in subs { let repository_clone = repository.clone(); let client_clone = client.clone(); - + let payload_clone = payload_string.clone(); let mailto_clone = mailto.clone(); let sub_clone = sub.clone(); - + // Clone the pre-parsed partial builder let pb_clone = pb.clone(); tokio::spawn(async move { - let clean_p256dh = sub_clone.p256dh.replace('+', "-").replace('/', "_").replace('=', ""); - let clean_auth = sub_clone.auth.replace('+', "-").replace('/', "_").replace('=', ""); - - let subscription_info = SubscriptionInfo::new( - sub_clone.endpoint.clone(), - clean_p256dh, - clean_auth, - ); + let clean_p256dh = sub_clone + .p256dh + .replace('+', "-") + .replace('/', "_") + .replace('=', ""); + let clean_auth = sub_clone + .auth + .replace('+', "-") + .replace('/', "_") + .replace('=', ""); + + let subscription_info = + SubscriptionInfo::new(sub_clone.endpoint.clone(), clean_p256dh, clean_auth); let mut sig_builder = pb_clone.add_sub_info(&subscription_info); sig_builder.add_claim("sub", mailto_clone.as_str()); @@ -144,7 +161,10 @@ pub async fn sender_worker( let signature = match sig_builder.build() { Ok(sig) => sig, Err(e) => { - info!("Sender error signing VAPID claims for device ID {}: {:?}", sub_clone.id, e); + info!( + "Sender error signing VAPID claims for device ID {}: {:?}", + sub_clone.id, e + ); return; } }; @@ -156,21 +176,36 @@ pub async fn sender_worker( let message = match builder.build() { Ok(msg) => msg, Err(e) => { - info!("Sender error building push message for device ID {}: {:?}", sub_clone.id, e); + info!( + "Sender error building push message for device ID {}: {:?}", + sub_clone.id, e + ); return; } }; match client_clone.send(message).await { Ok(_) => { - info!("Push notification sent successfully to device ID {}", sub_clone.id); + info!( + "Push notification sent successfully to device ID {}", + sub_clone.id + ); } - Err(WebPushError::EndpointNotValid(_)) | Err(WebPushError::EndpointNotFound(_)) => { - info!("Device subscription expired or invalid (404/410), deleting endpoint: {}", sub_clone.endpoint); - let _ = repository_clone.delete_stale_subscription(&sub_clone.endpoint).await; + Err(WebPushError::EndpointNotValid(_)) + | Err(WebPushError::EndpointNotFound(_)) => { + info!( + "Device subscription expired or invalid (404/410), deleting endpoint: {}", + sub_clone.endpoint + ); + let _ = repository_clone + .delete_stale_subscription(&sub_clone.endpoint) + .await; } Err(e) => { - info!("Failed to send Web Push to device ID {}: {:?}", sub_clone.id, e); + info!( + "Failed to send Web Push to device ID {}: {:?}", + sub_clone.id, e + ); } } }); diff --git a/backend/worker/src/tasks/verifier.rs b/backend/worker/src/tasks/verifier.rs index 62ee4c4..9837a2b 100644 --- a/backend/worker/src/tasks/verifier.rs +++ b/backend/worker/src/tasks/verifier.rs @@ -1,7 +1,7 @@ +use crate::tasks::communication::{TriggeredAlertNotification, VerifierToSender, WorkerToVerifier}; use log::info; -use crate::tasks::communication::{WorkerToVerifier, VerifierToSender, TriggeredAlertNotification}; -use repository::models::server::ServerStatus::{Offline, Online}; use repository::models::alert::AlertType; +use repository::models::server::ServerStatus::{Offline, Online}; use repository::postgres::PostgresRepository; use repository::repository::Repository; use tokio::sync::mpsc::{Receiver, Sender}; @@ -17,8 +17,8 @@ pub async fn verifier_worker( let WorkerToVerifier::ServerStatusUpdated(state) = message; let status_changed = state.new_status != state.old_status.clone().unwrap_or(Offline); - let player_number_changer = state.new_status == Online && - state.old_players.unwrap_or(0) != state.new_players.unwrap_or(0); + let player_number_changer = state.new_status == Online + && state.old_players.unwrap_or(0) != state.new_players.unwrap_or(0); // If nothing relevant changed, skip alert evaluation if !status_changed && !player_number_changer { @@ -30,7 +30,10 @@ pub async fn verifier_worker( let active_alerts = match active_alerts_result { Ok(alerts) => alerts, Err(e) => { - info!("Verifier error fetching alerts for server {}: {:?}", state.id, e); + info!( + "Verifier error fetching alerts for server {}: {:?}", + state.id, e + ); continue; } }; @@ -49,14 +52,14 @@ pub async fn verifier_worker( } AlertType::PlayerAbove => { if state.new_status == Online { - let threshold = alert.player_threshold.unwrap_or(0); + let threshold = alert.player_threshold.unwrap_or(0) as i32; is_triggered = state.new_players.unwrap_or(0) > threshold && state.old_players.unwrap_or(0) <= threshold; } } AlertType::PlayerBelow => { if state.new_status == Online { - let threshold = alert.player_threshold.unwrap_or(0); + let threshold = alert.player_threshold.unwrap_or(0) as i32; is_triggered = state.new_players.unwrap_or(0) < threshold && state.old_players.unwrap_or(0) >= threshold; } @@ -70,9 +73,17 @@ pub async fn verifier_worker( } if !triggered_notifications.is_empty() { - if let Err(e) = tx_sender.send(VerifierToSender::TriggerNotifications(triggered_notifications)).await { - info!("Verifier error sending batch notifications to Sender channel: {:?}", e); + if let Err(e) = tx_sender + .send(VerifierToSender::TriggerNotifications( + triggered_notifications, + )) + .await + { + info!( + "Verifier error sending batch notifications to Sender channel: {:?}", + e + ); } } } -} \ No newline at end of file +} diff --git a/frontend/app/core/lib/api/servers.ts b/frontend/app/core/lib/api/servers.ts index bc0bddc..5f3a624 100644 --- a/frontend/app/core/lib/api/servers.ts +++ b/frontend/app/core/lib/api/servers.ts @@ -2,9 +2,11 @@ import { z } from "zod"; import { PUBLIC_API_URL } from "@/core/lib/config"; import { API_BASE, getHeaders } from "./client"; import { ServerSchema, type Server, type ServerRecord } from "./schemas"; + export function getServerIconUrl(serverId: number | string): string { - return `${PUBLIC_API_URL}/servers/${serverId}/icon` + return `${PUBLIC_API_URL}/servers/${serverId}/icon?v=2` } + export function normalizeServerData(server: unknown): Server { if (server && typeof server === 'object' && 'data' in server) { const s = server as { data?: unknown }; diff --git a/frontend/app/routes/favicon.ts b/frontend/app/routes/favicon.ts index d27dccf..3123c1f 100644 --- a/frontend/app/routes/favicon.ts +++ b/frontend/app/routes/favicon.ts @@ -3,5 +3,5 @@ import { PUBLIC_API_URL } from "@/core/lib/config"; import type { Route } from "./+types/favicon"; export async function loader({ params }: Route.LoaderArgs) { - return redirect(`${PUBLIC_API_URL}/servers/${params.id}/icon`, 301); + return redirect(`${PUBLIC_API_URL}/servers/${params.id}/icon?v=2`, 301); }