From e1a136ff51f1cfb29428644caf1b680c5521cde2 Mon Sep 17 00:00:00 2001 From: Radical Date: Mon, 26 May 2025 13:22:56 +0000 Subject: [PATCH 001/160] feat: allow usage of local folder for file storage --- Cargo.toml | 1 + Dockerfile | 11 ++-- compose.dev.yml | 11 ++-- compose.yml | 11 ++-- entrypoint.sh | 6 ++- src/api/v1/servers/uuid/icon.rs | 3 +- src/api/v1/users/me.rs | 3 +- src/config.rs | 72 +++++++++++++------------ src/error.rs | 2 + src/main.rs | 24 ++++----- src/structs.rs | 95 +++++++++++++++++++++++++++------ 11 files changed, 158 insertions(+), 81 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7a062d8..18aa043 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ diesel-async = { version = "0.5", features = ["deadpool", "postgres", "async-con diesel_migrations = { version = "2.2.0", features = ["postgres"] } thiserror = "2.0.12" actix-multipart = "0.7.2" +actix-files = "0.6.6" [dependencies.tokio] version = "1.44" diff --git a/Dockerfile b/Dockerfile index 0f07fcb..e556fe6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,16 +18,17 @@ RUN useradd --create-home --home-dir /gorb gorb USER gorb -ENV DATABASE_USERNAME=gorb \ +ENV URL=http://localhost:8080 \ +DATABASE_USERNAME=gorb \ DATABASE_PASSWORD=gorb \ DATABASE=gorb \ DATABASE_HOST=database \ DATABASE_PORT=5432 \ CACHE_DB_HOST=valkey \ CACHE_DB_PORT=6379 \ -BUNNY_API_KEY=your_storage_zone_password_here \ -BUNNY_ENDPOINT=Frankfurt \ -BUNNY_ZONE=gorb \ -BUNNY_CDN_URL=https://cdn.gorb.app +BUNNY_API_KEY= \ +BUNNY_ENDPOINT= \ +BUNNY_ZONE= \ +BUNNY_CDN_URL= ENTRYPOINT ["/usr/bin/entrypoint.sh"] diff --git a/compose.dev.yml b/compose.dev.yml index 3da7c89..6b1ecda 100644 --- a/compose.dev.yml +++ b/compose.dev.yml @@ -18,15 +18,18 @@ services: - gorb-backend:/gorb environment: #- RUST_LOG=debug + # This should be changed to the public URL of the server! + - URL=http://localhost:8080 - DATABASE_USERNAME=gorb - DATABASE_PASSWORD=gorb - DATABASE=gorb - DATABASE_HOST=database - DATABASE_PORT=5432 - - BUNNY_API_KEY=your_storage_zone_password_here - - BUNNY_ENDPOINT=Frankfurt - - BUNNY_ZONE=gorb - - BUNNY_CDN_URL=https://cdn.gorb.app + # These can be set to use a CDN, if they are not set then files will be stored locally + #- BUNNY_API_KEY=your_storage_zone_password_here + #- BUNNY_ENDPOINT=Frankfurt + #- BUNNY_ZONE=gorb + #- BUNNY_CDN_URL=https://cdn.gorb.app database: image: postgres:16 restart: always diff --git a/compose.yml b/compose.yml index f87411a..438b4ba 100644 --- a/compose.yml +++ b/compose.yml @@ -16,15 +16,18 @@ services: - gorb-backend:/gorb environment: #- RUST_LOG=debug + # This should be changed to the public URL of the server! + - URL=http://localhost:8080 - DATABASE_USERNAME=gorb - DATABASE_PASSWORD=gorb - DATABASE=gorb - DATABASE_HOST=database - DATABASE_PORT=5432 - - BUNNY_API_KEY=your_storage_zone_password_here - - BUNNY_ENDPOINT=Frankfurt - - BUNNY_ZONE=gorb - - BUNNY_CDN_URL=https://cdn.gorb.app + # These can be set to use a CDN, if they are not set then files will be stored locally + #- BUNNY_API_KEY=your_storage_zone_password_here + #- BUNNY_ENDPOINT=Frankfurt + #- BUNNY_ZONE=gorb + #- BUNNY_CDN_URL=https://cdn.gorb.app database: image: postgres:16 restart: always diff --git a/entrypoint.sh b/entrypoint.sh index a29e6bb..82b8271 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -8,6 +8,10 @@ if [ ! -d "/gorb/logs" ]; then mkdir /gorb/logs fi +if [ ! -d "/gorb/data" ]; then + mkdir /gorb/data +fi + if [ ! -f "/gorb/config/config.toml" ]; then cat > /gorb/config/config.toml <&1 | tee /gorb/logs/backend.log +/usr/bin/gorb-backend --config /gorb/config/config.toml --data-dir /gorb/data 2>&1 | tee /gorb/logs/backend.log diff --git a/src/api/v1/servers/uuid/icon.rs b/src/api/v1/servers/uuid/icon.rs index 2155f55..66f2e8a 100644 --- a/src/api/v1/servers/uuid/icon.rs +++ b/src/api/v1/servers/uuid/icon.rs @@ -38,9 +38,8 @@ pub async fn upload( guild .set_icon( - &data.bunny_cdn, + &data.storage, &mut conn, - data.config.bunny.cdn_url.clone(), bytes, ) .await?; diff --git a/src/api/v1/users/me.rs b/src/api/v1/users/me.rs index 83d02db..fab34dd 100644 --- a/src/api/v1/users/me.rs +++ b/src/api/v1/users/me.rs @@ -58,9 +58,8 @@ pub async fn update( let byte_slice: &[u8] = &bytes; me.set_avatar( - &data.bunny_cdn, + &data.storage, &mut conn, - data.config.bunny.cdn_url.clone(), byte_slice.into(), ) .await?; diff --git a/src/config.rs b/src/config.rs index 102318f..9cefbe1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,8 +9,8 @@ use url::Url; pub struct ConfigBuilder { database: Database, cache_database: CacheDatabase, - web: Option, - bunny: BunnyBuilder, + web: WebBuilder, + bunny: Option, } #[derive(Debug, Deserialize, Clone)] @@ -33,8 +33,9 @@ pub struct CacheDatabase { #[derive(Debug, Deserialize)] struct WebBuilder { - url: Option, + ip: Option, port: Option, + url: Url, _ssl: Option, } @@ -57,37 +58,37 @@ impl ConfigBuilder { } pub fn build(self) -> Config { - let web = if let Some(web) = self.web { - Web { - url: web.url.unwrap_or(String::from("0.0.0.0")), - port: web.port.unwrap_or(8080), - } + let web = Web { + ip: self.web.ip.unwrap_or(String::from("0.0.0.0")), + port: self.web.port.unwrap_or(8080), + url: self.web.url + }; + + let bunny; + + if let Some(bunny_builder) = self.bunny { + let endpoint = match &*bunny_builder.endpoint { + "Frankfurt" => Endpoint::Frankfurt, + "London" => Endpoint::London, + "New York" => Endpoint::NewYork, + "Los Angeles" => Endpoint::LosAngeles, + "Singapore" => Endpoint::Singapore, + "Stockholm" => Endpoint::Stockholm, + "Sao Paulo" => Endpoint::SaoPaulo, + "Johannesburg" => Endpoint::Johannesburg, + "Sydney" => Endpoint::Sydney, + url => Endpoint::Custom(url.to_string()), + }; + + bunny = Some(Bunny { + api_key: bunny_builder.api_key, + endpoint, + storage_zone: bunny_builder.storage_zone, + cdn_url: bunny_builder.cdn_url, + }); } else { - Web { - url: String::from("0.0.0.0"), - port: 8080, - } - }; - - let endpoint = match &*self.bunny.endpoint { - "Frankfurt" => Endpoint::Frankfurt, - "London" => Endpoint::London, - "New York" => Endpoint::NewYork, - "Los Angeles" => Endpoint::LosAngeles, - "Singapore" => Endpoint::Singapore, - "Stockholm" => Endpoint::Stockholm, - "Sao Paulo" => Endpoint::SaoPaulo, - "Johannesburg" => Endpoint::Johannesburg, - "Sydney" => Endpoint::Sydney, - url => Endpoint::Custom(url.to_string()), - }; - - let bunny = Bunny { - api_key: self.bunny.api_key, - endpoint, - storage_zone: self.bunny.storage_zone, - cdn_url: self.bunny.cdn_url, - }; + bunny = None; + } Config { database: self.database, @@ -103,13 +104,14 @@ pub struct Config { pub database: Database, pub cache_database: CacheDatabase, pub web: Web, - pub bunny: Bunny, + pub bunny: Option, } #[derive(Debug, Clone)] pub struct Web { - pub url: String, + pub ip: String, pub port: u16, + pub url: Url, } #[derive(Debug, Clone)] diff --git a/src/error.rs b/src/error.rs index ce586ac..135982d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -55,6 +55,8 @@ pub enum Error { #[error("{0}")] PasswordHashError(String), #[error("{0}")] + PathError(String), + #[error("{0}")] BadRequest(String), #[error("{0}")] Unauthorized(String), diff --git a/src/main.rs b/src/main.rs index 5ad1dc8..74b1066 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ use actix_cors::Cors; +use actix_files::Files; use actix_web::{App, HttpServer, web}; use argon2::Argon2; use clap::Parser; @@ -6,6 +7,7 @@ use diesel_async::pooled_connection::AsyncDieselConnectionManager; use diesel_async::pooled_connection::deadpool::Pool; use error::Error; use simple_logger::SimpleLogger; +use structs::Storage; use std::time::SystemTime; mod config; use config::{Config, ConfigBuilder}; @@ -22,11 +24,13 @@ pub mod schema; pub mod structs; pub mod utils; -#[derive(Parser, Debug)] +#[derive(Parser, Debug, Clone)] #[command(version, about, long_about = None)] struct Args { #[arg(short, long, default_value_t = String::from("/etc/gorb/config.toml"))] config: String, + #[arg(short, long, default_value_t = String::from("/var/lib/gorb/"))] + data_dir: String, } #[derive(Clone)] @@ -39,7 +43,7 @@ pub struct Data { pub config: Config, pub argon2: Argon2<'static>, pub start_time: SystemTime, - pub bunny_cdn: bunny_api_tokio::Client, + pub storage: Storage, } #[tokio::main] @@ -63,14 +67,7 @@ async fn main() -> Result<(), Error> { let cache_pool = redis::Client::open(config.cache_database.url())?; - let mut bunny_cdn = bunny_api_tokio::Client::new("").await?; - - let bunny = config.bunny.clone(); - - bunny_cdn - .storage - .init(bunny.api_key, bunny.endpoint, bunny.storage_zone) - .await?; + let storage = Storage::new(config.clone(), args.data_dir.clone()).await?; let database_url = config.database.url(); @@ -111,9 +108,11 @@ async fn main() -> Result<(), Error> { // TODO: Possibly implement "pepper" into this (thinking it could generate one if it doesnt exist and store it on disk) argon2: Argon2::default(), start_time: SystemTime::now(), - bunny_cdn, + storage, }; + let data_dir = args.data_dir.clone(); + HttpServer::new(move || { // Set CORS headers let cors = Cors::default() @@ -143,9 +142,10 @@ async fn main() -> Result<(), Error> { App::new() .app_data(web::Data::new(data.clone())) .wrap(cors) + .service(Files::new("/api/assets", &data_dir)) .service(api::web()) }) - .bind((web.url, web.port))? + .bind((web.ip, web.port))? .run() .await?; diff --git a/src/structs.rs b/src/structs.rs index c86bc5b..92bc599 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -1,3 +1,5 @@ +use std::path::Path; + use actix_web::web::BytesMut; use diesel::{ ExpressionMethods, QueryDsl, Selectable, SelectableHelper, delete, insert_into, @@ -6,11 +8,11 @@ use diesel::{ }; use diesel_async::{RunQueryDsl, pooled_connection::AsyncDieselConnectionManager}; use serde::{Deserialize, Serialize}; -use tokio::task; +use tokio::{fs::{create_dir_all, remove_file, File}, io::AsyncWriteExt, task}; use url::Url; use uuid::Uuid; -use crate::{Conn, Data, error::Error, schema::*, utils::image_check}; +use crate::{config::Config, error::Error, schema::*, utils::image_check, Conn, Data}; fn load_or_empty( query_result: Result, diesel::result::Error>, @@ -22,6 +24,74 @@ fn load_or_empty( } } +#[derive(Clone)] +pub struct Storage { + bunny_cdn: Option, + cdn_url: Url, + data_dir: String, +} + +impl Storage { + pub async fn new(config: Config, data_dir: String) -> Result { + let mut bunny_cdn; + let cdn_url; + + if let Some(bunny) = config.bunny { + bunny_cdn = Some(bunny_api_tokio::Client::new("").await?); + + bunny_cdn + .as_mut() + .unwrap() + .storage + .init(bunny.api_key, bunny.endpoint, bunny.storage_zone) + .await?; + + cdn_url = bunny.cdn_url; + } else { + bunny_cdn = None; + cdn_url = config.web.url.join("api/assets/")?; + } + + Ok(Self { + bunny_cdn, + data_dir, + cdn_url + }) + } + + pub async fn write>(&self, path: T, bytes: BytesMut) -> Result { + if let Some(bunny_cdn) = &self.bunny_cdn { + bunny_cdn.storage.upload(&path, bytes.into()).await?; + Ok(self.cdn_url.join(&path.as_ref())?) + } else { + let file_path = Path::new(&self.data_dir); + + let file_path = file_path.join(path.as_ref()); + + create_dir_all(file_path.parent().ok_or(Error::PathError("Unable to get parent directory".to_string()))?).await?; + + let mut file = File::create(file_path).await?; + + file.write_all(&bytes).await?; + + Ok(self.cdn_url.join(path.as_ref())?) + } + } + + pub async fn delete>(&self, path: T) -> Result<(), Error> { + if let Some(bunny_cdn) = &self.bunny_cdn { + Ok(bunny_cdn.storage.delete(&path).await?) + } else { + let file_path = Path::new(&self.data_dir); + + let file_path = file_path.join(path.as_ref()); + + remove_file(file_path).await?; + Ok(()) + } + } +} + #[derive(Queryable, Selectable, Insertable, Clone)] #[diesel(table_name = channels)] #[diesel(check_for_backend(diesel::pg::Pg))] @@ -419,9 +489,8 @@ impl Guild { // FIXME: Horrible security pub async fn set_icon( &mut self, - bunny_cdn: &bunny_api_tokio::Client, + storage: &Storage, conn: &mut Conn, - cdn_url: Url, icon: BytesMut, ) -> Result<(), Error> { let icon_clone = icon.clone(); @@ -430,14 +499,12 @@ impl Guild { if let Some(icon) = &self.icon { let relative_url = icon.path().trim_start_matches('/'); - bunny_cdn.storage.delete(relative_url).await?; + storage.delete(relative_url).await?; } let path = format!("icons/{}/icon.{}", self.uuid, image_type); - bunny_cdn.storage.upload(path.clone(), icon.into()).await?; - - let icon_url = cdn_url.join(&path)?; + let icon_url = storage.write(path.clone(), icon.into()).await?; use guilds::dsl; update(guilds::table) @@ -673,9 +740,8 @@ impl Me { pub async fn set_avatar( &mut self, - bunny_cdn: &bunny_api_tokio::Client, + storage: &Storage, conn: &mut Conn, - cdn_url: Url, avatar: BytesMut, ) -> Result<(), Error> { let avatar_clone = avatar.clone(); @@ -686,18 +752,15 @@ impl Me { let relative_url = avatar_url.path().trim_start_matches('/'); - bunny_cdn.storage.delete(relative_url).await?; + storage.delete(relative_url).await?; } let path = format!("avatar/{}/avatar.{}", self.uuid, image_type); - bunny_cdn - .storage - .upload(path.clone(), avatar.into()) + let avatar_url = storage + .write(path.clone(), avatar.into()) .await?; - let avatar_url = cdn_url.join(&path)?; - use users::dsl; update(users::table) .filter(dsl::uuid.eq(self.uuid)) From 03dd1d06e285fafb811548d417651cc6b105993e Mon Sep 17 00:00:00 2001 From: Radical Date: Mon, 26 May 2025 13:53:46 +0000 Subject: [PATCH 002/160] fix: update entrypoint.sh to not insert bunny when unused --- entrypoint.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/entrypoint.sh b/entrypoint.sh index 82b8271..5cf3dc3 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -14,6 +14,9 @@ fi if [ ! -f "/gorb/config/config.toml" ]; then cat > /gorb/config/config.toml <> "/gorb/config/config.toml" < Date: Mon, 26 May 2025 13:54:09 +0000 Subject: [PATCH 003/160] fix: use correct env var in dockerfile/compose --- Dockerfile | 2 +- compose.dev.yml | 2 +- compose.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index e556fe6..f76653a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ RUN useradd --create-home --home-dir /gorb gorb USER gorb -ENV URL=http://localhost:8080 \ +ENV WEB_URL=http://localhost:8080 \ DATABASE_USERNAME=gorb \ DATABASE_PASSWORD=gorb \ DATABASE=gorb \ diff --git a/compose.dev.yml b/compose.dev.yml index 6b1ecda..af28fcc 100644 --- a/compose.dev.yml +++ b/compose.dev.yml @@ -19,7 +19,7 @@ services: environment: #- RUST_LOG=debug # This should be changed to the public URL of the server! - - URL=http://localhost:8080 + - WEB_URL=http://localhost:8080 - DATABASE_USERNAME=gorb - DATABASE_PASSWORD=gorb - DATABASE=gorb diff --git a/compose.yml b/compose.yml index 438b4ba..e3ad269 100644 --- a/compose.yml +++ b/compose.yml @@ -17,7 +17,7 @@ services: environment: #- RUST_LOG=debug # This should be changed to the public URL of the server! - - URL=http://localhost:8080 + - WEB_URL=http://localhost:8080 - DATABASE_USERNAME=gorb - DATABASE_PASSWORD=gorb - DATABASE=gorb From 5d26f94cdd7fea5fbe336637be635eb986d312cb Mon Sep 17 00:00:00 2001 From: Radical Date: Mon, 26 May 2025 19:17:36 +0200 Subject: [PATCH 004/160] style: use ? operator instead of unwrap in websockets --- .../v1/servers/uuid/channels/uuid/socket.rs | 32 ++++++++++--------- src/error.rs | 2 ++ 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/api/v1/servers/uuid/channels/uuid/socket.rs b/src/api/v1/servers/uuid/channels/uuid/socket.rs index 8938842..3300e6c 100644 --- a/src/api/v1/servers/uuid/channels/uuid/socket.rs +++ b/src/api/v1/servers/uuid/channels/uuid/socket.rs @@ -62,49 +62,51 @@ pub async fn echo( let mut session_2 = session_1.clone(); rt::spawn(async move { - pubsub.subscribe(channel_uuid.to_string()).await.unwrap(); + pubsub.subscribe(channel_uuid.to_string()).await?; while let Some(msg) = pubsub.on_message().next().await { - let payload: String = msg.get_payload().unwrap(); - session_1.text(payload).await.unwrap(); + let payload: String = msg.get_payload()?; + session_1.text(payload).await?; } + + Ok::<(), crate::error::Error>(()) }); // start task but don't wait for it rt::spawn(async move { - let mut conn = data - .cache_pool - .get_multiplexed_tokio_connection() - .await - .unwrap(); // receive messages from websocket while let Some(msg) = stream.next().await { match msg { Ok(AggregatedMessage::Text(text)) => { - // echo text message + let mut conn = data + .cache_pool + .get_multiplexed_tokio_connection() + .await?; + redis::cmd("PUBLISH") .arg(&[channel_uuid.to_string(), text.to_string()]) .exec_async(&mut conn) - .await - .unwrap(); + .await?; + channel .new_message(&mut data.pool.get().await.unwrap(), uuid, text.to_string()) - .await - .unwrap(); + .await?; } Ok(AggregatedMessage::Binary(bin)) => { // echo binary message - session_2.binary(bin).await.unwrap(); + session_2.binary(bin).await?; } Ok(AggregatedMessage::Ping(msg)) => { // respond to PING frame with PONG frame - session_2.pong(&msg).await.unwrap(); + session_2.pong(&msg).await?; } _ => {} } } + + Ok::<(), crate::error::Error>(()) }); // respond immediately with response connected to WS session diff --git a/src/error.rs b/src/error.rs index ce586ac..fa9524f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -52,6 +52,8 @@ pub enum Error { UrlParseError(#[from] url::ParseError), #[error(transparent)] PayloadError(#[from] PayloadError), + #[error(transparent)] + WsClosed(#[from] actix_ws::Closed), #[error("{0}")] PasswordHashError(String), #[error("{0}")] From efa0cd555f41858dbd35470860754cedad517625 Mon Sep 17 00:00:00 2001 From: Radical Date: Mon, 26 May 2025 19:41:32 +0200 Subject: [PATCH 005/160] fix: hack around websocket spec to make tokens work --- .../v1/servers/uuid/channels/uuid/socket.rs | 24 ++++++++----- src/utils.rs | 34 +++++++++++++++++++ 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src/api/v1/servers/uuid/channels/uuid/socket.rs b/src/api/v1/servers/uuid/channels/uuid/socket.rs index 3300e6c..c16efa7 100644 --- a/src/api/v1/servers/uuid/channels/uuid/socket.rs +++ b/src/api/v1/servers/uuid/channels/uuid/socket.rs @@ -1,4 +1,8 @@ -use actix_web::{Error, HttpRequest, HttpResponse, get, rt, web}; +use actix_web::{ + Error, HttpRequest, HttpResponse, get, + http::header::{HeaderValue, SEC_WEBSOCKET_PROTOCOL}, + rt, web, +}; use actix_ws::AggregatedMessage; use futures_util::StreamExt as _; use uuid::Uuid; @@ -7,7 +11,7 @@ use crate::{ Data, api::v1::auth::check_access_token, structs::{Channel, Member}, - utils::get_auth_header, + utils::get_ws_protocol_header, }; #[get("{uuid}/channels/{channel_uuid}/socket")] @@ -21,7 +25,7 @@ pub async fn echo( let headers = req.headers(); // Retrieve auth header - let auth_header = get_auth_header(headers)?; + let auth_header = get_ws_protocol_header(headers)?; // Get uuids from path let (guild_uuid, channel_uuid) = path.into_inner(); @@ -46,7 +50,7 @@ pub async fn echo( .await?; } - let (res, mut session_1, stream) = actix_ws::handle(&req, stream)?; + let (mut res, mut session_1, stream) = actix_ws::handle(&req, stream)?; let mut stream = stream .aggregate_continuations() @@ -77,10 +81,7 @@ pub async fn echo( while let Some(msg) = stream.next().await { match msg { Ok(AggregatedMessage::Text(text)) => { - let mut conn = data - .cache_pool - .get_multiplexed_tokio_connection() - .await?; + let mut conn = data.cache_pool.get_multiplexed_tokio_connection().await?; redis::cmd("PUBLISH") .arg(&[channel_uuid.to_string(), text.to_string()]) @@ -109,6 +110,13 @@ pub async fn echo( Ok::<(), crate::error::Error>(()) }); + let headers = res.headers_mut(); + + headers.append( + SEC_WEBSOCKET_PROTOCOL, + HeaderValue::from_str("Authorization")?, + ); + // respond immediately with response connected to WS session Ok(res) } diff --git a/src/utils.rs b/src/utils.rs index f9a5705..631b003 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -45,6 +45,40 @@ pub fn get_auth_header(headers: &HeaderMap) -> Result<&str, Error> { Ok(auth_value.unwrap()) } +pub fn get_ws_protocol_header(headers: &HeaderMap) -> Result<&str, Error> { + let auth_token = headers.get(actix_web::http::header::SEC_WEBSOCKET_PROTOCOL); + + if auth_token.is_none() { + return Err(Error::Unauthorized( + "No authorization header provided".to_string(), + )); + } + + let auth_raw = auth_token.unwrap().to_str()?; + + let mut auth = auth_raw.split_whitespace(); + + let response_proto = auth.next(); + + let auth_value = auth.next(); + + if response_proto.is_none() { + return Err(Error::BadRequest( + "Sec-WebSocket-Protocol header is empty".to_string(), + )); + } else if response_proto.is_some_and(|rp| rp != "Authorization,") { + return Err(Error::BadRequest( + "First protocol should be Authorization".to_string(), + )); + } + + if auth_value.is_none() { + return Err(Error::BadRequest("No token provided".to_string())); + } + + Ok(auth_value.unwrap()) +} + pub fn refresh_token_cookie(refresh_token: String) -> Cookie<'static> { Cookie::build("refresh_token", refresh_token) .http_only(true) From bcb82d0f4609c2333a0dea608d25a3438883855b Mon Sep 17 00:00:00 2001 From: Radical Date: Mon, 26 May 2025 21:32:43 +0200 Subject: [PATCH 006/160] fix: return message struct to websocket connection --- src/api/v1/servers/uuid/channels/uuid/socket.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/api/v1/servers/uuid/channels/uuid/socket.rs b/src/api/v1/servers/uuid/channels/uuid/socket.rs index c16efa7..744f017 100644 --- a/src/api/v1/servers/uuid/channels/uuid/socket.rs +++ b/src/api/v1/servers/uuid/channels/uuid/socket.rs @@ -83,13 +83,13 @@ pub async fn echo( Ok(AggregatedMessage::Text(text)) => { let mut conn = data.cache_pool.get_multiplexed_tokio_connection().await?; - redis::cmd("PUBLISH") - .arg(&[channel_uuid.to_string(), text.to_string()]) - .exec_async(&mut conn) + let message = channel + .new_message(&mut data.pool.get().await?, uuid, text.to_string()) .await?; - channel - .new_message(&mut data.pool.get().await.unwrap(), uuid, text.to_string()) + redis::cmd("PUBLISH") + .arg(&[channel_uuid.to_string(), serde_json::to_string(&message)?]) + .exec_async(&mut conn) .await?; } From d8541b2eea923f67070700ae05e14b54eaa4ad4d Mon Sep 17 00:00:00 2001 From: Radical Date: Mon, 26 May 2025 22:26:16 +0200 Subject: [PATCH 007/160] feat: add channel ordering --- .../down.sql | 2 + .../up.sql | 2 + src/api/v1/servers/uuid/channels/mod.rs | 16 ++++--- src/error.rs | 2 + src/schema.rs | 1 + src/structs.rs | 42 ++++++++++++++++--- src/utils.rs | 24 ++++++++++- 7 files changed, 74 insertions(+), 15 deletions(-) create mode 100644 migrations/2025-05-26-181536_add_channel_ordering/down.sql create mode 100644 migrations/2025-05-26-181536_add_channel_ordering/up.sql diff --git a/migrations/2025-05-26-181536_add_channel_ordering/down.sql b/migrations/2025-05-26-181536_add_channel_ordering/down.sql new file mode 100644 index 0000000..0a70d35 --- /dev/null +++ b/migrations/2025-05-26-181536_add_channel_ordering/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE channels DROP COLUMN is_above; diff --git a/migrations/2025-05-26-181536_add_channel_ordering/up.sql b/migrations/2025-05-26-181536_add_channel_ordering/up.sql new file mode 100644 index 0000000..e18e5e2 --- /dev/null +++ b/migrations/2025-05-26-181536_add_channel_ordering/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE channels ADD COLUMN is_above UUID UNIQUE REFERENCES channels(uuid) DEFAULT NULL; diff --git a/src/api/v1/servers/uuid/channels/mod.rs b/src/api/v1/servers/uuid/channels/mod.rs index 0c515ef..3021ded 100644 --- a/src/api/v1/servers/uuid/channels/mod.rs +++ b/src/api/v1/servers/uuid/channels/mod.rs @@ -1,9 +1,5 @@ use crate::{ - Data, - api::v1::auth::check_access_token, - error::Error, - structs::{Channel, Member}, - utils::get_auth_header, + api::v1::auth::check_access_token, error::Error, structs::{Channel, Member}, utils::{get_auth_header, order_channels}, Data }; use ::uuid::Uuid; use actix_web::{HttpRequest, HttpResponse, get, post, web}; @@ -43,10 +39,12 @@ pub async fn get( let channels = Channel::fetch_all(&data.pool, guild_uuid).await?; - data.set_cache_key(format!("{}_channels", guild_uuid), channels.clone(), 1800) + let channels_ordered = order_channels(channels).await?; + + data.set_cache_key(format!("{}_channels", guild_uuid), channels_ordered.clone(), 1800) .await?; - Ok(HttpResponse::Ok().json(channels)) + Ok(HttpResponse::Ok().json(channels_ordered)) } #[post("{uuid}/channels")] @@ -76,7 +74,7 @@ pub async fn create( channel_info.name.clone(), channel_info.description.clone(), ) - .await; + .await?; - Ok(HttpResponse::Ok().json(channel.unwrap())) + Ok(HttpResponse::Ok().json(channel)) } diff --git a/src/error.rs b/src/error.rs index fa9524f..3907f7c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -60,6 +60,8 @@ pub enum Error { BadRequest(String), #[error("{0}")] Unauthorized(String), + #[error("{0}")] + InternalServerError(String), } impl ResponseError for Error { diff --git a/src/schema.rs b/src/schema.rs index 33935f7..8a85a2e 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -27,6 +27,7 @@ diesel::table! { name -> Varchar, #[max_length = 500] description -> Nullable, + is_above -> Nullable, } } diff --git a/src/structs.rs b/src/structs.rs index c86bc5b..b423b20 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -5,12 +5,18 @@ use diesel::{ update, }; use diesel_async::{RunQueryDsl, pooled_connection::AsyncDieselConnectionManager}; +use log::debug; use serde::{Deserialize, Serialize}; use tokio::task; use url::Url; use uuid::Uuid; -use crate::{Conn, Data, error::Error, schema::*, utils::image_check}; +use crate::{ + Conn, Data, + error::Error, + schema::*, + utils::{image_check, order_channels}, +}; fn load_or_empty( query_result: Result, diesel::result::Error>, @@ -22,7 +28,7 @@ fn load_or_empty( } } -#[derive(Queryable, Selectable, Insertable, Clone)] +#[derive(Queryable, Selectable, Insertable, Clone, Debug)] #[diesel(table_name = channels)] #[diesel(check_for_backend(diesel::pg::Pg))] struct ChannelBuilder { @@ -30,6 +36,7 @@ struct ChannelBuilder { guild_uuid: Uuid, name: String, description: Option, + is_above: Option, } impl ChannelBuilder { @@ -48,21 +55,23 @@ impl ChannelBuilder { guild_uuid: self.guild_uuid, name: self.name, description: self.description, + is_above: self.is_above, permissions: channel_permission, }) } } -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone, Debug)] pub struct Channel { pub uuid: Uuid, pub guild_uuid: Uuid, name: String, description: Option, + pub is_above: Option, pub permissions: Vec, } -#[derive(Serialize, Deserialize, Clone, Queryable, Selectable)] +#[derive(Serialize, Deserialize, Clone, Queryable, Selectable, Debug)] #[diesel(table_name = channel_permissions)] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct ChannelPermission { @@ -118,24 +127,47 @@ impl Channel { let channel_uuid = Uuid::now_v7(); + let channels = Self::fetch_all(&data.pool, guild_uuid).await?; + + debug!("{:?}", channels); + + let channels_ordered = order_channels(channels).await?; + + debug!("{:?}", channels_ordered); + + let last_channel = channels_ordered.last(); + let new_channel = ChannelBuilder { uuid: channel_uuid, guild_uuid, name: name.clone(), description: description.clone(), + is_above: None, }; + debug!("New Channel: {:?}", new_channel); + insert_into(channels::table) - .values(new_channel) + .values(new_channel.clone()) .execute(&mut conn) .await?; + if let Some(old_last_channel) = last_channel { + use channels::dsl; + update(channels::table) + .filter(dsl::uuid.eq(old_last_channel.uuid)) + .set(dsl::is_above.eq(new_channel.uuid)) + .execute(&mut conn) + .await?; + } + // returns different object because there's no reason to build the channelbuilder (wastes 1 database request) let channel = Self { uuid: channel_uuid, guild_uuid, name, description, + is_above: None, permissions: vec![], }; diff --git a/src/utils.rs b/src/utils.rs index 631b003..e68f38a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -9,7 +9,7 @@ use hex::encode; use redis::RedisError; use serde::Serialize; -use crate::{Data, error::Error}; +use crate::{Data, error::Error, structs::Channel}; pub fn get_auth_header(headers: &HeaderMap) -> Result<&str, Error> { let auth_token = headers.get(actix_web::http::header::AUTHORIZATION); @@ -119,6 +119,28 @@ pub fn image_check(icon: BytesMut) -> Result { )) } +pub async fn order_channels(mut channels: Vec) -> Result, Error> { + let mut ordered = Vec::new(); + + // Find head + let head_pos = channels + .iter() + .position(|channel| !channels.iter().any(|i| i.is_above == Some(channel.uuid))); + + if let Some(pos) = head_pos { + ordered.push(channels.swap_remove(pos)); + + while let Some(next_pos) = channels + .iter() + .position(|channel| Some(channel.uuid) == ordered.last().unwrap().is_above) + { + ordered.push(channels.swap_remove(next_pos)); + } + } + + Ok(ordered) +} + impl Data { pub async fn set_cache_key( &self, From 1cda34d16bccc6d0d1f7606ded545200d88ae1ba Mon Sep 17 00:00:00 2001 From: Radical Date: Mon, 26 May 2025 22:26:47 +0200 Subject: [PATCH 008/160] fix: remove more unwraps found more unwraps that needed to be changed to ? --- src/api/v1/auth/refresh.rs | 9 ++------- src/api/v1/servers/mod.rs | 2 +- src/api/v1/servers/uuid/channels/mod.rs | 14 +++++++++++--- src/api/v1/servers/uuid/channels/uuid/mod.rs | 2 +- src/api/v1/servers/uuid/channels/uuid/socket.rs | 2 +- src/api/v1/servers/uuid/roles/mod.rs | 2 +- 6 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/api/v1/auth/refresh.rs b/src/api/v1/auth/refresh.rs index 303748a..fceabf5 100644 --- a/src/api/v1/auth/refresh.rs +++ b/src/api/v1/auth/refresh.rs @@ -59,14 +59,9 @@ pub async fn res(req: HttpRequest, data: web::Data) -> Result 1987200 { - let new_refresh_token = generate_refresh_token(); + let new_refresh_token = generate_refresh_token()?; - if new_refresh_token.is_err() { - error!("{}", new_refresh_token.unwrap_err()); - return Ok(HttpResponse::InternalServerError().finish()); - } - - let new_refresh_token = new_refresh_token.unwrap(); + let new_refresh_token = new_refresh_token; match update(refresh_tokens::table) .filter(rdsl::token.eq(&refresh_token)) diff --git a/src/api/v1/servers/mod.rs b/src/api/v1/servers/mod.rs index 76a4c16..91980e3 100644 --- a/src/api/v1/servers/mod.rs +++ b/src/api/v1/servers/mod.rs @@ -63,7 +63,7 @@ pub async fn get( let amount = request_query.amount.unwrap_or(10); - check_access_token(auth_header, &mut data.pool.get().await.unwrap()).await?; + check_access_token(auth_header, &mut data.pool.get().await?).await?; let guilds = Guild::fetch_amount(&data.pool, start, amount).await?; diff --git a/src/api/v1/servers/uuid/channels/mod.rs b/src/api/v1/servers/uuid/channels/mod.rs index 3021ded..c3640fe 100644 --- a/src/api/v1/servers/uuid/channels/mod.rs +++ b/src/api/v1/servers/uuid/channels/mod.rs @@ -1,5 +1,9 @@ use crate::{ - api::v1::auth::check_access_token, error::Error, structs::{Channel, Member}, utils::{get_auth_header, order_channels}, Data + Data, + api::v1::auth::check_access_token, + error::Error, + structs::{Channel, Member}, + utils::{get_auth_header, order_channels}, }; use ::uuid::Uuid; use actix_web::{HttpRequest, HttpResponse, get, post, web}; @@ -41,8 +45,12 @@ pub async fn get( let channels_ordered = order_channels(channels).await?; - data.set_cache_key(format!("{}_channels", guild_uuid), channels_ordered.clone(), 1800) - .await?; + data.set_cache_key( + format!("{}_channels", guild_uuid), + channels_ordered.clone(), + 1800, + ) + .await?; Ok(HttpResponse::Ok().json(channels_ordered)) } diff --git a/src/api/v1/servers/uuid/channels/uuid/mod.rs b/src/api/v1/servers/uuid/channels/uuid/mod.rs index 54f90a7..b1c749f 100644 --- a/src/api/v1/servers/uuid/channels/uuid/mod.rs +++ b/src/api/v1/servers/uuid/channels/uuid/mod.rs @@ -64,7 +64,7 @@ pub async fn delete( let channel: Channel; if let Ok(cache_hit) = data.get_cache_key(format!("{}", channel_uuid)).await { - channel = serde_json::from_str(&cache_hit).unwrap(); + channel = serde_json::from_str(&cache_hit)?; data.del_cache_key(format!("{}", channel_uuid)).await?; } else { diff --git a/src/api/v1/servers/uuid/channels/uuid/socket.rs b/src/api/v1/servers/uuid/channels/uuid/socket.rs index 744f017..a90cb86 100644 --- a/src/api/v1/servers/uuid/channels/uuid/socket.rs +++ b/src/api/v1/servers/uuid/channels/uuid/socket.rs @@ -42,7 +42,7 @@ pub async fn echo( // Return channel cache or result from psql as `channel` variable if let Ok(cache_hit) = data.get_cache_key(format!("{}", channel_uuid)).await { - channel = serde_json::from_str(&cache_hit).unwrap() + channel = serde_json::from_str(&cache_hit)? } else { channel = Channel::fetch_one(&mut conn, channel_uuid).await?; diff --git a/src/api/v1/servers/uuid/roles/mod.rs b/src/api/v1/servers/uuid/roles/mod.rs index fe25d39..db6c7cf 100644 --- a/src/api/v1/servers/uuid/roles/mod.rs +++ b/src/api/v1/servers/uuid/roles/mod.rs @@ -62,7 +62,7 @@ pub async fn create( let guild_uuid = path.into_inner().0; - let mut conn = data.pool.get().await.unwrap(); + let mut conn = data.pool.get().await?; let uuid = check_access_token(auth_header, &mut conn).await?; From b8cf21903ebd2c362775f47b4804020caf7ce7cd Mon Sep 17 00:00:00 2001 From: Radical Date: Mon, 26 May 2025 23:41:20 +0200 Subject: [PATCH 009/160] feat: allow disabling of registration --- src/api/v1/auth/register.rs | 4 ++++ src/config.rs | 8 ++++++++ src/error.rs | 2 ++ 3 files changed, 14 insertions(+) diff --git a/src/api/v1/auth/register.rs b/src/api/v1/auth/register.rs index 75aeb9d..66be90a 100644 --- a/src/api/v1/auth/register.rs +++ b/src/api/v1/auth/register.rs @@ -67,6 +67,10 @@ pub async fn res( account_information: web::Json, data: web::Data, ) -> Result { + if !data.config.instance.registration { + return Err(Error::Forbidden("registration is disabled on this instance".to_string())) + } + let uuid = Uuid::now_v7(); if !EMAIL_REGEX.is_match(&account_information.email) { diff --git a/src/config.rs b/src/config.rs index 102318f..079ce35 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,6 +10,7 @@ pub struct ConfigBuilder { database: Database, cache_database: CacheDatabase, web: Option, + instance: Option, bunny: BunnyBuilder, } @@ -38,6 +39,11 @@ struct WebBuilder { _ssl: Option, } +#[derive(Debug, Deserialize, Clone)] +pub struct Instance { + pub registration: bool, +} + #[derive(Debug, Deserialize)] struct BunnyBuilder { api_key: String, @@ -93,6 +99,7 @@ impl ConfigBuilder { database: self.database, cache_database: self.cache_database, web, + instance: self.instance.unwrap_or(Instance { registration: true }), bunny, } } @@ -103,6 +110,7 @@ pub struct Config { pub database: Database, pub cache_database: CacheDatabase, pub web: Web, + pub instance: Instance, pub bunny: Bunny, } diff --git a/src/error.rs b/src/error.rs index 3907f7c..fb990c7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -61,6 +61,8 @@ pub enum Error { #[error("{0}")] Unauthorized(String), #[error("{0}")] + Forbidden(String), + #[error("{0}")] InternalServerError(String), } From 39d01bb0d04883b1d4789b2b64c8039b493e4441 Mon Sep 17 00:00:00 2001 From: Radical Date: Tue, 27 May 2025 07:46:10 +0000 Subject: [PATCH 010/160] feat: move me endpoint to /me and add /me/servers --- src/api/v1/me/friends/mod.rs | 0 src/api/v1/me/friends/pending.rs | 0 src/api/v1/{users/me.rs => me/mod.rs} | 16 +++++++++--- src/api/v1/me/servers.rs | 35 +++++++++++++++++++++++++++ src/api/v1/mod.rs | 2 ++ src/api/v1/users/mod.rs | 3 --- src/structs.rs | 13 +++++++++- 7 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 src/api/v1/me/friends/mod.rs create mode 100644 src/api/v1/me/friends/pending.rs rename src/api/v1/{users/me.rs => me/mod.rs} (88%) create mode 100644 src/api/v1/me/servers.rs diff --git a/src/api/v1/me/friends/mod.rs b/src/api/v1/me/friends/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/api/v1/me/friends/pending.rs b/src/api/v1/me/friends/pending.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/api/v1/users/me.rs b/src/api/v1/me/mod.rs similarity index 88% rename from src/api/v1/users/me.rs rename to src/api/v1/me/mod.rs index 83d02db..14067c3 100644 --- a/src/api/v1/users/me.rs +++ b/src/api/v1/me/mod.rs @@ -1,13 +1,21 @@ use actix_multipart::form::{MultipartForm, json::Json as MpJson, tempfile::TempFile}; -use actix_web::{HttpRequest, HttpResponse, get, patch, web}; +use actix_web::{get, patch, web, HttpRequest, HttpResponse, Scope}; use serde::Deserialize; use crate::{ Data, api::v1::auth::check_access_token, error::Error, structs::Me, utils::get_auth_header, }; -#[get("/me")] -pub async fn res(req: HttpRequest, data: web::Data) -> Result { +mod servers; + +pub fn web() -> Scope { + web::scope("/me") + .service(get) + .service(update) +} + +#[get("")] +pub async fn get(req: HttpRequest, data: web::Data) -> Result { let headers = req.headers(); let auth_header = get_auth_header(headers)?; @@ -36,7 +44,7 @@ struct UploadForm { json: Option>, } -#[patch("/me")] +#[patch("")] pub async fn update( req: HttpRequest, MultipartForm(form): MultipartForm, diff --git a/src/api/v1/me/servers.rs b/src/api/v1/me/servers.rs new file mode 100644 index 0000000..73ab139 --- /dev/null +++ b/src/api/v1/me/servers.rs @@ -0,0 +1,35 @@ +//! Contains endpoint related to guild memberships + +use actix_web::{get, web, HttpRequest, HttpResponse}; + +use crate::{api::v1::auth::check_access_token, error::Error, structs::Me, utils::get_auth_header, Data}; + + +/// `GET /api/v1/me/servers` Returns all guild memberships in a list +/// +/// Example Response: +/// ``` +/// json!({ +/// "uuid": "22006503-fb01-46e6-8e0e-70336dac6c63", +/// "nickname": "This field is nullable", +/// "user_uuid": "522bca17-de63-4706-9d18-0971867ad1e0", +/// "guild_uuid": "0911e468-3e9e-47bf-8381-59b30e8b68a8" +/// }); +/// ``` +/// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps +#[get("/servers")] +pub async fn get(req: HttpRequest, data: web::Data) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + let me = Me::get(&mut conn, uuid).await?; + + let memberships = me.fetch_memberships(&mut conn).await?; + + Ok(HttpResponse::Ok().json(memberships)) +} \ No newline at end of file diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs index 749774d..f711fe5 100644 --- a/src/api/v1/mod.rs +++ b/src/api/v1/mod.rs @@ -5,6 +5,7 @@ mod invites; mod servers; mod stats; mod users; +mod me; pub fn web() -> Scope { web::scope("/v1") @@ -13,4 +14,5 @@ pub fn web() -> Scope { .service(users::web()) .service(servers::web()) .service(invites::web()) + .service(me::web()) } diff --git a/src/api/v1/users/mod.rs b/src/api/v1/users/mod.rs index a0b1ea6..a81491c 100644 --- a/src/api/v1/users/mod.rs +++ b/src/api/v1/users/mod.rs @@ -8,14 +8,11 @@ use crate::{ utils::get_auth_header, }; -mod me; mod uuid; pub fn web() -> Scope { web::scope("/users") .service(res) - .service(me::res) - .service(me::update) .service(uuid::res) } diff --git a/src/structs.rs b/src/structs.rs index b423b20..fc1d9ca 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -542,7 +542,7 @@ impl Role { } } -#[derive(Queryable, Selectable, Insertable)] +#[derive(Serialize, Queryable, Selectable, Insertable)] #[diesel(table_name = guild_members)] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct Member { @@ -703,6 +703,17 @@ impl Me { Ok(me) } + pub async fn fetch_memberships(&self, conn: &mut Conn) -> Result, Error> { + use guild_members::dsl; + let memberships: Vec = dsl::guild_members + .filter(dsl::user_uuid.eq(self.uuid)) + .select(Member::as_select()) + .load(conn) + .await?; + + Ok(memberships) + } + pub async fn set_avatar( &mut self, bunny_cdn: &bunny_api_tokio::Client, From 1aa38631b8b0f125599ef83d90eba5d66d28d2d0 Mon Sep 17 00:00:00 2001 From: Radical Date: Tue, 27 May 2025 11:16:33 +0000 Subject: [PATCH 011/160] feat: implement is_above for roles and reuse same functions from channels! --- .../down.sql | 3 + .../up.sql | 3 + src/api/v1/servers/mod.rs | 2 - src/api/v1/servers/uuid/channels/mod.rs | 8 +- src/api/v1/servers/uuid/roles/mod.rs | 12 ++- src/schema.rs | 2 +- src/structs.rs | 73 ++++++++++++++----- src/utils.rs | 19 +++-- 8 files changed, 81 insertions(+), 41 deletions(-) create mode 100644 migrations/2025-05-27-105059_redo_role_ordering/down.sql create mode 100644 migrations/2025-05-27-105059_redo_role_ordering/up.sql diff --git a/migrations/2025-05-27-105059_redo_role_ordering/down.sql b/migrations/2025-05-27-105059_redo_role_ordering/down.sql new file mode 100644 index 0000000..6b38e1e --- /dev/null +++ b/migrations/2025-05-27-105059_redo_role_ordering/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE roles ADD COLUMN position int NOT NULL DEFAULT 0; +ALTER TABLE roles DROP COLUMN is_above; diff --git a/migrations/2025-05-27-105059_redo_role_ordering/up.sql b/migrations/2025-05-27-105059_redo_role_ordering/up.sql new file mode 100644 index 0000000..d426ab7 --- /dev/null +++ b/migrations/2025-05-27-105059_redo_role_ordering/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE roles DROP COLUMN position; +ALTER TABLE roles ADD COLUMN is_above UUID UNIQUE REFERENCES roles(uuid) DEFAULT NULL; diff --git a/src/api/v1/servers/mod.rs b/src/api/v1/servers/mod.rs index 91980e3..987d439 100644 --- a/src/api/v1/servers/mod.rs +++ b/src/api/v1/servers/mod.rs @@ -14,7 +14,6 @@ use crate::{ #[derive(Deserialize)] struct GuildInfo { name: String, - description: Option, } pub fn web() -> Scope { @@ -41,7 +40,6 @@ pub async fn create( let guild = Guild::new( &mut conn, guild_info.name.clone(), - guild_info.description.clone(), uuid, ) .await?; diff --git a/src/api/v1/servers/uuid/channels/mod.rs b/src/api/v1/servers/uuid/channels/mod.rs index c3640fe..d87aba2 100644 --- a/src/api/v1/servers/uuid/channels/mod.rs +++ b/src/api/v1/servers/uuid/channels/mod.rs @@ -1,9 +1,5 @@ use crate::{ - Data, - api::v1::auth::check_access_token, - error::Error, - structs::{Channel, Member}, - utils::{get_auth_header, order_channels}, + api::v1::auth::check_access_token, error::Error, structs::{Channel, Member}, utils::{get_auth_header, order_by_is_above}, Data }; use ::uuid::Uuid; use actix_web::{HttpRequest, HttpResponse, get, post, web}; @@ -43,7 +39,7 @@ pub async fn get( let channels = Channel::fetch_all(&data.pool, guild_uuid).await?; - let channels_ordered = order_channels(channels).await?; + let channels_ordered = order_by_is_above(channels).await?; data.set_cache_key( format!("{}_channels", guild_uuid), diff --git a/src/api/v1/servers/uuid/roles/mod.rs b/src/api/v1/servers/uuid/roles/mod.rs index db6c7cf..c0ebaa2 100644 --- a/src/api/v1/servers/uuid/roles/mod.rs +++ b/src/api/v1/servers/uuid/roles/mod.rs @@ -3,11 +3,7 @@ use actix_web::{HttpRequest, HttpResponse, get, post, web}; use serde::Deserialize; use crate::{ - Data, - api::v1::auth::check_access_token, - error::Error, - structs::{Member, Role}, - utils::get_auth_header, + api::v1::auth::check_access_token, error::Error, structs::{Member, Role}, utils::{get_auth_header, order_by_is_above}, Data }; pub mod uuid; @@ -43,10 +39,12 @@ pub async fn get( let roles = Role::fetch_all(&mut conn, guild_uuid).await?; - data.set_cache_key(format!("{}_roles", guild_uuid), roles.clone(), 1800) + let roles_ordered = order_by_is_above(roles).await?; + + data.set_cache_key(format!("{}_roles", guild_uuid), roles_ordered.clone(), 1800) .await?; - Ok(HttpResponse::Ok().json(roles)) + Ok(HttpResponse::Ok().json(roles_ordered)) } #[post("{uuid}/roles")] diff --git a/src/schema.rs b/src/schema.rs index 8a85a2e..cc5e97c 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -105,8 +105,8 @@ diesel::table! { #[max_length = 50] name -> Varchar, color -> Int4, - position -> Int4, permissions -> Int8, + is_above -> Nullable, } } diff --git a/src/structs.rs b/src/structs.rs index fc1d9ca..be4bd43 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -5,7 +5,6 @@ use diesel::{ update, }; use diesel_async::{RunQueryDsl, pooled_connection::AsyncDieselConnectionManager}; -use log::debug; use serde::{Deserialize, Serialize}; use tokio::task; use url::Url; @@ -15,9 +14,17 @@ use crate::{ Conn, Data, error::Error, schema::*, - utils::{image_check, order_channels}, + utils::{image_check, order_by_is_above}, }; +pub trait HasUuid { + fn uuid(&self) -> &Uuid; +} + +pub trait HasIsAbove { + fn is_above(&self) -> Option<&Uuid>; +} + fn load_or_empty( query_result: Result, diesel::result::Error>, ) -> Result, diesel::result::Error> { @@ -79,6 +86,18 @@ pub struct ChannelPermission { pub permissions: i64, } +impl HasUuid for Channel { + fn uuid(&self) -> &Uuid { + self.uuid.as_ref() + } +} + +impl HasIsAbove for Channel { + fn is_above(&self) -> Option<&Uuid> { + self.is_above.as_ref() + } +} + impl Channel { pub async fn fetch_all( pool: &deadpool::managed::Pool< @@ -129,11 +148,7 @@ impl Channel { let channels = Self::fetch_all(&data.pool, guild_uuid).await?; - debug!("{:?}", channels); - - let channels_ordered = order_channels(channels).await?; - - debug!("{:?}", channels_ordered); + let channels_ordered = order_by_is_above(channels).await?; let last_channel = channels_ordered.last(); @@ -145,8 +160,6 @@ impl Channel { is_above: None, }; - debug!("New Channel: {:?}", new_channel); - insert_into(channels::table) .values(new_channel.clone()) .execute(&mut conn) @@ -359,7 +372,6 @@ impl Guild { pub async fn new( conn: &mut Conn, name: String, - description: Option, owner_uuid: Uuid, ) -> Result { let guild_uuid = Uuid::now_v7(); @@ -367,7 +379,7 @@ impl Guild { let guild_builder = GuildBuilder { uuid: guild_uuid, name: name.clone(), - description: description.clone(), + description: None, icon: None, owner_uuid, }; @@ -394,7 +406,7 @@ impl Guild { Ok(Guild { uuid: guild_uuid, name, - description, + description: None, icon: None, owner_uuid, roles: vec![], @@ -492,10 +504,22 @@ pub struct Role { guild_uuid: Uuid, name: String, color: i32, - position: i32, + is_above: Option, permissions: i64, } +impl HasUuid for Role { + fn uuid(&self) -> &Uuid { + self.uuid.as_ref() + } +} + +impl HasIsAbove for Role { + fn is_above(&self) -> Option<&Uuid> { + self.is_above.as_ref() + } +} + impl Role { pub async fn fetch_all(conn: &mut Conn, guild_uuid: Uuid) -> Result, Error> { use roles::dsl; @@ -524,21 +548,36 @@ impl Role { pub async fn new(conn: &mut Conn, guild_uuid: Uuid, name: String) -> Result { let role_uuid = Uuid::now_v7(); - let role = Role { + let roles = Self::fetch_all(conn, guild_uuid).await?; + + let roles_ordered = order_by_is_above(roles).await?; + + let last_role = roles_ordered.last(); + + let new_role = Role { uuid: role_uuid, guild_uuid, name, color: 16777215, - position: 0, + is_above: None, permissions: 0, }; insert_into(roles::table) - .values(role.clone()) + .values(new_role.clone()) .execute(conn) .await?; - Ok(role) + if let Some(old_last_role) = last_role { + use roles::dsl; + update(roles::table) + .filter(dsl::uuid.eq(old_last_role.uuid)) + .set(dsl::is_above.eq(new_role.uuid)) + .execute(conn) + .await?; + } + + Ok(new_role) } } diff --git a/src/utils.rs b/src/utils.rs index e68f38a..3edaab8 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -9,7 +9,7 @@ use hex::encode; use redis::RedisError; use serde::Serialize; -use crate::{Data, error::Error, structs::Channel}; +use crate::{error::Error, structs::{HasIsAbove, HasUuid}, Data}; pub fn get_auth_header(headers: &HeaderMap) -> Result<&str, Error> { let auth_token = headers.get(actix_web::http::header::AUTHORIZATION); @@ -119,22 +119,25 @@ pub fn image_check(icon: BytesMut) -> Result { )) } -pub async fn order_channels(mut channels: Vec) -> Result, Error> { +pub async fn order_by_is_above(mut items: Vec) -> Result, Error> +where + T: HasUuid + HasIsAbove, +{ let mut ordered = Vec::new(); // Find head - let head_pos = channels + let head_pos = items .iter() - .position(|channel| !channels.iter().any(|i| i.is_above == Some(channel.uuid))); + .position(|item| !items.iter().any(|i| i.is_above() == Some(item.uuid()))); if let Some(pos) = head_pos { - ordered.push(channels.swap_remove(pos)); + ordered.push(items.swap_remove(pos)); - while let Some(next_pos) = channels + while let Some(next_pos) = items .iter() - .position(|channel| Some(channel.uuid) == ordered.last().unwrap().is_above) + .position(|item| Some(item.uuid()) == ordered.last().unwrap().is_above()) { - ordered.push(channels.swap_remove(next_pos)); + ordered.push(items.swap_remove(next_pos)); } } From 16ccf94631f36be237d2aa94a7f125bec72f1b02 Mon Sep 17 00:00:00 2001 From: Radical Date: Tue, 27 May 2025 11:52:17 +0000 Subject: [PATCH 012/160] docs: partially document codebase Should make it easier for frontend to figure out what stuff actually does, more will be added as the project goes on --- src/api/mod.rs | 4 +- src/api/v1/me/servers.rs | 26 +++++++---- src/api/v1/mod.rs | 2 + src/api/v1/servers/mod.rs | 77 ++++++++++++++++++++++++++++++++- src/api/v1/servers/uuid/icon.rs | 7 +++ src/api/v1/servers/uuid/mod.rs | 39 ++++++++++++++++- src/api/v1/stats.rs | 15 +++++++ src/api/v1/users/mod.rs | 38 ++++++++++++++-- src/api/v1/users/uuid.rs | 20 ++++++++- src/api/versions.rs | 16 ++++++- 10 files changed, 226 insertions(+), 18 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index 25391eb..87c1c14 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,3 +1,5 @@ +//! `/api` Contains the entire API + use actix_web::Scope; use actix_web::web; @@ -5,5 +7,5 @@ mod v1; mod versions; pub fn web() -> Scope { - web::scope("/api").service(v1::web()).service(versions::res) + web::scope("/api").service(v1::web()).service(versions::get) } diff --git a/src/api/v1/me/servers.rs b/src/api/v1/me/servers.rs index 73ab139..390c31e 100644 --- a/src/api/v1/me/servers.rs +++ b/src/api/v1/me/servers.rs @@ -1,4 +1,4 @@ -//! Contains endpoint related to guild memberships +//! `/api/v1/me/servers` Contains endpoint related to guild memberships use actix_web::{get, web, HttpRequest, HttpResponse}; @@ -7,14 +7,24 @@ use crate::{api::v1::auth::check_access_token, error::Error, structs::Me, utils: /// `GET /api/v1/me/servers` Returns all guild memberships in a list /// -/// Example Response: +/// requires auth: yes +/// +/// ### Example Response /// ``` -/// json!({ -/// "uuid": "22006503-fb01-46e6-8e0e-70336dac6c63", -/// "nickname": "This field is nullable", -/// "user_uuid": "522bca17-de63-4706-9d18-0971867ad1e0", -/// "guild_uuid": "0911e468-3e9e-47bf-8381-59b30e8b68a8" -/// }); +/// json!([ +/// { +/// "uuid": "22006503-fb01-46e6-8e0e-70336dac6c63", +/// "nickname": "This field is nullable", +/// "user_uuid": "522bca17-de63-4706-9d18-0971867ad1e0", +/// "guild_uuid": "0911e468-3e9e-47bf-8381-59b30e8b68a8" +/// }, +/// { +/// "uuid": "bf95361e-3b64-4704-969c-3c5a80d10514", +/// "nickname": null, +/// "user_uuid": "522bca17-de63-4706-9d18-0971867ad1e0", +/// "guild_uuid": "69ec2ce5-3d8b-4451-b644-c2d969905458" +/// } +/// ]); /// ``` /// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps #[get("/servers")] diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs index f711fe5..f30ad58 100644 --- a/src/api/v1/mod.rs +++ b/src/api/v1/mod.rs @@ -1,3 +1,5 @@ +//! `/api/v1` Contains version 1 of the api + use actix_web::{Scope, web}; mod auth; diff --git a/src/api/v1/servers/mod.rs b/src/api/v1/servers/mod.rs index 987d439..e864d0a 100644 --- a/src/api/v1/servers/mod.rs +++ b/src/api/v1/servers/mod.rs @@ -1,3 +1,5 @@ +//! `/api/v1/servers` Guild related endpoints + use actix_web::{HttpRequest, HttpResponse, Scope, get, post, web}; use serde::Deserialize; @@ -18,13 +20,37 @@ struct GuildInfo { pub fn web() -> Scope { web::scope("/servers") - .service(create) + .service(post) .service(get) .service(uuid::web()) } +/// `POST /api/v1/servers` Creates a new guild +/// +/// requires auth: yes +/// +/// ### Request Example +/// ``` +/// json!({ +/// "name": "My new server!" +/// }); +/// ``` +/// +/// ### Response Example +/// ``` +/// json!({ +/// "uuid": "383d2afa-082f-4dd3-9050-ca6ed91487b6", +/// "name": "My new server!", +/// "description": null, +/// "icon": null, +/// "owner_uuid": "155d2291-fb23-46bd-a656-ae7c5d8218e6", +/// "roles": [], +/// "member_count": 1 +/// }); +/// ``` +/// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps #[post("")] -pub async fn create( +pub async fn post( req: HttpRequest, guild_info: web::Json, data: web::Data, @@ -47,6 +73,53 @@ pub async fn create( Ok(HttpResponse::Ok().json(guild)) } +/// `GET /api/v1/servers` Fetches all guilds +/// +/// requires auth: yes +/// +/// requires admin: yes +/// +/// ### Response Example +/// ``` +/// json!([ +/// { +/// "uuid": "383d2afa-082f-4dd3-9050-ca6ed91487b6", +/// "name": "My new server!", +/// "description": null, +/// "icon": null, +/// "owner_uuid": "155d2291-fb23-46bd-a656-ae7c5d8218e6", +/// "roles": [], +/// "member_count": 1 +/// }, +/// { +/// "uuid": "5ba61ec7-5f97-43e1-89a5-d4693c155612", +/// "name": "My first server!", +/// "description": "This is a cool and nullable description!", +/// "icon": "https://nullable-url/path/to/icon.png", +/// "owner_uuid": "155d2291-fb23-46bd-a656-ae7c5d8218e6", +/// "roles": [ +/// { +/// "uuid": "be0e4da4-cf73-4f45-98f8-bb1c73d1ab8b", +/// "guild_uuid": "5ba61ec7-5f97-43e1-89a5-d4693c155612", +/// "name": "Cool people", +/// "color": 15650773, +/// "is_above": c7432f1c-f4ad-4ad3-8216-51388b6abb5b, +/// "permissions": 0 +/// } +/// { +/// "uuid": "c7432f1c-f4ad-4ad3-8216-51388b6abb5b", +/// "guild_uuid": "5ba61ec7-5f97-43e1-89a5-d4693c155612", +/// "name": "Equally cool people", +/// "color": 16777215, +/// "is_above": null, +/// "permissions": 0 +/// } +/// ], +/// "member_count": 20 +/// } +/// ]); +/// ``` +/// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps #[get("")] pub async fn get( req: HttpRequest, diff --git a/src/api/v1/servers/uuid/icon.rs b/src/api/v1/servers/uuid/icon.rs index 2155f55..af9ab46 100644 --- a/src/api/v1/servers/uuid/icon.rs +++ b/src/api/v1/servers/uuid/icon.rs @@ -1,3 +1,5 @@ +//! `/api/v1/servers/{uuid}/icon` icon related endpoints, will probably be replaced by a multipart post to above endpoint + use actix_web::{HttpRequest, HttpResponse, put, web}; use futures_util::StreamExt as _; use uuid::Uuid; @@ -10,6 +12,11 @@ use crate::{ utils::get_auth_header, }; +/// `PUT /api/v1/servers/{uuid}/icon` Icon upload +/// +/// requires auth: no +/// +/// put request expects a file and nothing else #[put("{uuid}/icon")] pub async fn upload( req: HttpRequest, diff --git a/src/api/v1/servers/uuid/mod.rs b/src/api/v1/servers/uuid/mod.rs index 887fd06..80b3068 100644 --- a/src/api/v1/servers/uuid/mod.rs +++ b/src/api/v1/servers/uuid/mod.rs @@ -1,3 +1,5 @@ +//! `/api/v1/servers/{uuid}` Specific server endpoints + use actix_web::{HttpRequest, HttpResponse, Scope, get, web}; use uuid::Uuid; @@ -17,7 +19,7 @@ use crate::{ pub fn web() -> Scope { web::scope("") // Servers - .service(res) + .service(get) // Channels .service(channels::get) .service(channels::create) @@ -36,8 +38,41 @@ pub fn web() -> Scope { .service(icon::upload) } +/// `GET /api/v1/servers/{uuid}` DESCRIPTION +/// +/// requires auth: yes +/// +/// ### Response Example +/// ``` +/// json!({ +/// "uuid": "5ba61ec7-5f97-43e1-89a5-d4693c155612", +/// "name": "My first server!", +/// "description": "This is a cool and nullable description!", +/// "icon": "https://nullable-url/path/to/icon.png", +/// "owner_uuid": "155d2291-fb23-46bd-a656-ae7c5d8218e6", +/// "roles": [ +/// { +/// "uuid": "be0e4da4-cf73-4f45-98f8-bb1c73d1ab8b", +/// "guild_uuid": "5ba61ec7-5f97-43e1-89a5-d4693c155612", +/// "name": "Cool people", +/// "color": 15650773, +/// "is_above": c7432f1c-f4ad-4ad3-8216-51388b6abb5b, +/// "permissions": 0 +/// } +/// { +/// "uuid": "c7432f1c-f4ad-4ad3-8216-51388b6abb5b", +/// "guild_uuid": "5ba61ec7-5f97-43e1-89a5-d4693c155612", +/// "name": "Equally cool people", +/// "color": 16777215, +/// "is_above": null, +/// "permissions": 0 +/// } +/// ], +/// "member_count": 20 +/// }); +/// ``` #[get("/{uuid}")] -pub async fn res( +pub async fn get( req: HttpRequest, path: web::Path<(Uuid,)>, data: web::Data, diff --git a/src/api/v1/stats.rs b/src/api/v1/stats.rs index 0b9567e..4e8aa32 100644 --- a/src/api/v1/stats.rs +++ b/src/api/v1/stats.rs @@ -1,3 +1,5 @@ +//! `/api/v1/stats` Returns stats about the server + use std::time::SystemTime; use actix_web::{HttpResponse, get, web}; @@ -19,6 +21,19 @@ struct Response { build_number: String, } +/// `GET /api/v1/` Returns stats about the server +/// +/// requires auth: no +/// +/// ### Response Example +/// ``` +/// json!({ +/// "accounts": 3, +/// "uptime": 50000, +/// "version": "0.1.0", +/// "build_number": "39d01bb" +/// }); +/// ``` #[get("/stats")] pub async fn res(data: web::Data) -> Result { let accounts: i64 = users diff --git a/src/api/v1/users/mod.rs b/src/api/v1/users/mod.rs index a81491c..0b80fd1 100644 --- a/src/api/v1/users/mod.rs +++ b/src/api/v1/users/mod.rs @@ -1,3 +1,5 @@ +//! `/api/v1/users` Contains endpoints related to all users + use actix_web::{HttpRequest, HttpResponse, Scope, get, web}; use crate::{ @@ -12,12 +14,42 @@ mod uuid; pub fn web() -> Scope { web::scope("/users") - .service(res) - .service(uuid::res) + .service(get) + .service(uuid::get) } +/// `GET /api/v1/users` Returns all users on this instance +/// +/// requires auth: yes +/// +/// requires admin: yes +/// +/// ### Response Example +/// ``` +/// json!([ +/// { +/// "uuid": "155d2291-fb23-46bd-a656-ae7c5d8218e6", +/// "username": "user1", +/// "display_name": "Nullable Name", +/// "avatar": "https://nullable-url.com/path/to/image.png" +/// }, +/// { +/// "uuid": "d48a3317-7b4d-443f-a250-ea9ab2bb8661", +/// "username": "user2", +/// "display_name": "John User 2", +/// "avatar": "https://also-supports-jpg.com/path/to/image.jpg" +/// }, +/// { +/// "uuid": "12c4b3f8-a25b-4b9b-8136-b275c855ed4a", +/// "username": "user3", +/// "display_name": null, +/// "avatar": null +/// } +/// ]); +/// ``` +/// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps #[get("")] -pub async fn res( +pub async fn get( req: HttpRequest, request_query: web::Query, data: web::Data, diff --git a/src/api/v1/users/uuid.rs b/src/api/v1/users/uuid.rs index 6d9f904..27e3326 100644 --- a/src/api/v1/users/uuid.rs +++ b/src/api/v1/users/uuid.rs @@ -1,3 +1,5 @@ +//! `/api/v1/users/{uuid}` Specific user endpoints + use actix_web::{HttpRequest, HttpResponse, get, web}; use uuid::Uuid; @@ -5,8 +7,24 @@ use crate::{ Data, api::v1::auth::check_access_token, error::Error, structs::User, utils::get_auth_header, }; +/// `GET /api/v1/users/{uuid}` Returns user with the given UUID +/// +/// requires auth: yes +/// +/// requires relation: yes +/// +/// ### Response Example +/// ``` +/// json!({ +/// "uuid": "155d2291-fb23-46bd-a656-ae7c5d8218e6", +/// "username": "user1", +/// "display_name": "Nullable Name", +/// "avatar": "https://nullable-url.com/path/to/image.png" +/// }); +/// ``` +/// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps #[get("/{uuid}")] -pub async fn res( +pub async fn get( req: HttpRequest, path: web::Path<(Uuid,)>, data: web::Data, diff --git a/src/api/versions.rs b/src/api/versions.rs index e5695be..809d6ed 100644 --- a/src/api/versions.rs +++ b/src/api/versions.rs @@ -1,3 +1,4 @@ +//! `/api/v1/versions` Returns info about api versions use actix_web::{HttpResponse, Responder, get}; use serde::Serialize; @@ -10,8 +11,21 @@ struct Response { #[derive(Serialize)] struct UnstableFeatures; +/// `GET /api/versions` Returns info about api versions. +/// +/// requires auth: no +/// +/// ### Response Example +/// ``` +/// json!({ +/// "unstable_features": {}, +/// "versions": [ +/// "1" +/// ] +/// }); +/// ``` #[get("/versions")] -pub async fn res() -> impl Responder { +pub async fn get() -> impl Responder { let response = Response { unstable_features: UnstableFeatures, // TODO: Find a way to dynamically update this possibly? From 862e2d6709be8d652de282260cf40a0baa738ddc Mon Sep 17 00:00:00 2001 From: Radical Date: Tue, 27 May 2025 13:59:06 +0000 Subject: [PATCH 013/160] feat: add mail client Untested --- Cargo.toml | 1 + src/api/v1/me/mod.rs | 6 +---- src/config.rs | 24 ++++++++++++++++++ src/error.rs | 5 ++++ src/main.rs | 7 ++++++ src/structs.rs | 58 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 96 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7a062d8..af5b2ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ diesel-async = { version = "0.5", features = ["deadpool", "postgres", "async-con diesel_migrations = { version = "2.2.0", features = ["postgres"] } thiserror = "2.0.12" actix-multipart = "0.7.2" +lettre = { version = "0.11.16", features = ["tokio1", "tokio1-native-tls"] } [dependencies.tokio] version = "1.44" diff --git a/src/api/v1/me/mod.rs b/src/api/v1/me/mod.rs index 14067c3..58eaa02 100644 --- a/src/api/v1/me/mod.rs +++ b/src/api/v1/me/mod.rs @@ -33,7 +33,7 @@ pub async fn get(req: HttpRequest, data: web::Data) -> Result, display_name: Option, - password: Option, + //password: Option, will probably be handled through a reset password link email: Option, } @@ -83,10 +83,6 @@ pub async fn update( todo!(); } - if let Some(password) = &new_info.password { - todo!(); - } - if let Some(email) = &new_info.email { todo!(); } diff --git a/src/config.rs b/src/config.rs index 079ce35..4d2e96b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,6 @@ use crate::error::Error; use bunny_api_tokio::edge_storage::Endpoint; +use lettre::transport::smtp::authentication::Credentials; use log::debug; use serde::Deserialize; use tokio::fs::read_to_string; @@ -12,6 +13,7 @@ pub struct ConfigBuilder { web: Option, instance: Option, bunny: BunnyBuilder, + mail: Mail, } #[derive(Debug, Deserialize, Clone)] @@ -52,6 +54,20 @@ struct BunnyBuilder { cdn_url: Url, } +#[derive(Debug, Deserialize, Clone)] +pub struct Mail { + pub smtp: Smtp, + pub from: String, + pub tls: String, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct Smtp { + pub server: String, + username: String, + password: String, +} + impl ConfigBuilder { pub async fn load(path: String) -> Result { debug!("loading config from: {}", path); @@ -101,6 +117,7 @@ impl ConfigBuilder { web, instance: self.instance.unwrap_or(Instance { registration: true }), bunny, + mail: self.mail, } } } @@ -112,6 +129,7 @@ pub struct Config { pub web: Web, pub instance: Instance, pub bunny: Bunny, + pub mail: Mail, } #[derive(Debug, Clone)] @@ -179,3 +197,9 @@ impl CacheDatabase { url } } + +impl Smtp { + pub fn credentials(&self) -> Credentials { + Credentials::new(self.username.clone(), self.password.clone()) + } +} diff --git a/src/error.rs b/src/error.rs index fb990c7..ca05c98 100644 --- a/src/error.rs +++ b/src/error.rs @@ -19,6 +19,7 @@ use serde_json::Error as JsonError; use thiserror::Error; use tokio::task::JoinError; use toml::de::Error as TomlError; +use lettre::{address::AddressError, transport::smtp::Error as SmtpError}; #[derive(Debug, Error)] pub enum Error { @@ -54,6 +55,10 @@ pub enum Error { PayloadError(#[from] PayloadError), #[error(transparent)] WsClosed(#[from] actix_ws::Closed), + #[error(transparent)] + SmtpError(#[from] SmtpError), + #[error(transparent)] + SmtpAddressError(#[from] AddressError), #[error("{0}")] PasswordHashError(String), #[error("{0}")] diff --git a/src/main.rs b/src/main.rs index 5ad1dc8..8bc1c68 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ use diesel_async::pooled_connection::AsyncDieselConnectionManager; use diesel_async::pooled_connection::deadpool::Pool; use error::Error; use simple_logger::SimpleLogger; +use structs::MailClient; use std::time::SystemTime; mod config; use config::{Config, ConfigBuilder}; @@ -40,6 +41,7 @@ pub struct Data { pub argon2: Argon2<'static>, pub start_time: SystemTime, pub bunny_cdn: bunny_api_tokio::Client, + pub mail_client: MailClient, } #[tokio::main] @@ -72,6 +74,10 @@ async fn main() -> Result<(), Error> { .init(bunny.api_key, bunny.endpoint, bunny.storage_zone) .await?; + let mail = config.mail.clone(); + + let mail_client = MailClient::new(mail.smtp.credentials(), mail.smtp.server, mail.from, mail.tls)?; + let database_url = config.database.url(); tokio::task::spawn_blocking(move || { @@ -112,6 +118,7 @@ async fn main() -> Result<(), Error> { argon2: Argon2::default(), start_time: SystemTime::now(), bunny_cdn, + mail_client, }; HttpServer::new(move || { diff --git a/src/structs.rs b/src/structs.rs index be4bd43..541d6ec 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -5,6 +5,8 @@ use diesel::{ update, }; use diesel_async::{RunQueryDsl, pooled_connection::AsyncDieselConnectionManager}; +use lettre::{message::{Mailbox, MessageBuilder as EmailBuilder}, transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message as Email, Tokio1Executor}; +use log::debug; use serde::{Deserialize, Serialize}; use tokio::task; use url::Url; @@ -35,6 +37,62 @@ fn load_or_empty( } } +#[derive(PartialEq, Eq, Clone)] +pub enum MailTls { + StartTls, + Tls, +} + +impl From for MailTls { + fn from(value: String) -> Self { + match &*value.to_lowercase() { + "starttls" => Self::StartTls, + _ => Self::Tls, + } + } +} + +#[derive(Clone)] +pub struct MailClient { + creds: Credentials, + smtp_server: String, + mbox: Mailbox, + tls: MailTls, +} + +impl MailClient { + pub fn new>(creds: Credentials, smtp_server: String, mbox: String, tls: T) -> Result { + Ok(Self { + creds, + smtp_server, + mbox: mbox.parse()?, + tls: tls.into(), + }) + } + + pub async fn message_builder(&self) -> EmailBuilder { + Email::builder() + .from(self.mbox.clone()) + } + + pub async fn send_mail(&self, email: Email) -> Result<(), Error> { + let mailer: AsyncSmtpTransport = match self.tls { + MailTls::StartTls => AsyncSmtpTransport::::starttls_relay(&self.smtp_server)? + .credentials(self.creds.clone()) + .build(), + MailTls::Tls => AsyncSmtpTransport::::relay(&self.smtp_server)? + .credentials(self.creds.clone()) + .build(), + }; + + let response = mailer.send(email).await?; + + debug!("mail sending response: {:?}", response); + + Ok(()) + } +} + #[derive(Queryable, Selectable, Insertable, Clone, Debug)] #[diesel(table_name = channels)] #[diesel(check_for_backend(diesel::pg::Pg))] From 83f031779f5b307ccd8e653ac79ff05655f94a9a Mon Sep 17 00:00:00 2001 From: Radical Date: Tue, 27 May 2025 21:57:08 +0200 Subject: [PATCH 014/160] feat: add email verification system Co-Authored-By: JustTemmie --- Cargo.toml | 7 +- Dockerfile | 10 +- compose.dev.yml | 6 + compose.yml | 6 + entrypoint.sh | 12 ++ .../down.sql | 2 + .../up.sql | 7 ++ src/api/v1/auth/mod.rs | 3 + src/api/v1/auth/verify_email.rs | 103 ++++++++++++++++++ src/config.rs | 24 ++-- src/error.rs | 8 +- src/main.rs | 4 +- src/schema.rs | 11 ++ src/structs.rs | 95 ++++++++++++++-- 14 files changed, 265 insertions(+), 33 deletions(-) create mode 100644 migrations/2025-05-27-162114_create_email_tokens/down.sql create mode 100644 migrations/2025-05-27-162114_create_email_tokens/up.sql create mode 100644 src/api/v1/auth/verify_email.rs diff --git a/Cargo.toml b/Cargo.toml index af5b2ff..492a284 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,20 +25,21 @@ redis = { version = "0.31.0", features= ["tokio-comp"] } tokio-tungstenite = { version = "0.26", features = ["native-tls", "url"] } toml = "0.8" url = { version = "2.5", features = ["serde"] } -uuid = { version = "1.16", features = ["serde", "v7"] } +uuid = { version = "1.17", features = ["serde", "v7"] } random-string = "1.1" actix-ws = "0.3.0" futures-util = "0.3.31" bunny-api-tokio = "0.3.0" bindet = "0.3.2" deadpool = "0.12" -diesel = { version = "2.2", features = ["uuid"] } +diesel = { version = "2.2", features = ["uuid", "chrono"] } diesel-async = { version = "0.5", features = ["deadpool", "postgres", "async-connection-wrapper"] } diesel_migrations = { version = "2.2.0", features = ["postgres"] } thiserror = "2.0.12" actix-multipart = "0.7.2" lettre = { version = "0.11.16", features = ["tokio1", "tokio1-native-tls"] } +chrono = { version = "0.4.41", features = ["serde"] } [dependencies.tokio] -version = "1.44" +version = "1.45" features = ["full"] diff --git a/Dockerfile b/Dockerfile index 0f07fcb..8ea076f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,8 @@ RUN useradd --create-home --home-dir /gorb gorb USER gorb -ENV DATABASE_USERNAME=gorb \ +ENV WEB_URL=https://gorb.app/web/ \ +DATABASE_USERNAME=gorb \ DATABASE_PASSWORD=gorb \ DATABASE=gorb \ DATABASE_HOST=database \ @@ -28,6 +29,11 @@ CACHE_DB_PORT=6379 \ BUNNY_API_KEY=your_storage_zone_password_here \ BUNNY_ENDPOINT=Frankfurt \ BUNNY_ZONE=gorb \ -BUNNY_CDN_URL=https://cdn.gorb.app +BUNNY_CDN_URL=https://cdn.gorb.app \ +MAIL_ADDRESS=Gorb \ +MAIL_TLS=tls \ +SMTP_SERVER=mail.gorb.app \ +SMTP_USERNAME=your_smtp_username \ +SMTP_PASSWORD=your_smtp_password \ ENTRYPOINT ["/usr/bin/entrypoint.sh"] diff --git a/compose.dev.yml b/compose.dev.yml index 3da7c89..e80f2a7 100644 --- a/compose.dev.yml +++ b/compose.dev.yml @@ -18,6 +18,7 @@ services: - gorb-backend:/gorb environment: #- RUST_LOG=debug + - WEB_URL=https://gorb.app/web/ - DATABASE_USERNAME=gorb - DATABASE_PASSWORD=gorb - DATABASE=gorb @@ -27,6 +28,11 @@ services: - BUNNY_ENDPOINT=Frankfurt - BUNNY_ZONE=gorb - BUNNY_CDN_URL=https://cdn.gorb.app + - MAIL_ADDRESS=Gorb + - MAIL_TLS=tls + - SMTP_SERVER=mail.gorb.app + - SMTP_USERNAME=your_smtp_username + - SMTP_PASSWORD=your_smtp_password database: image: postgres:16 restart: always diff --git a/compose.yml b/compose.yml index f87411a..2bc7339 100644 --- a/compose.yml +++ b/compose.yml @@ -16,6 +16,7 @@ services: - gorb-backend:/gorb environment: #- RUST_LOG=debug + - WEB_URL=https://gorb.app/web/ - DATABASE_USERNAME=gorb - DATABASE_PASSWORD=gorb - DATABASE=gorb @@ -25,6 +26,11 @@ services: - BUNNY_ENDPOINT=Frankfurt - BUNNY_ZONE=gorb - BUNNY_CDN_URL=https://cdn.gorb.app + - MAIL_ADDRESS=Gorb + - MAIL_TLS=tls + - SMTP_SERVER=mail.gorb.app + - SMTP_USERNAME=your_smtp_username + - SMTP_PASSWORD=your_smtp_password database: image: postgres:16 restart: always diff --git a/entrypoint.sh b/entrypoint.sh index a29e6bb..9c7a401 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -10,6 +10,9 @@ fi if [ ! -f "/gorb/config/config.toml" ]; then cat > /gorb/config/config.toml < Scope { .service(login::response) .service(refresh::res) .service(revoke::res) + .service(verify_email::get) + .service(verify_email::post) } pub async fn check_access_token(access_token: &str, conn: &mut Conn) -> Result { diff --git a/src/api/v1/auth/verify_email.rs b/src/api/v1/auth/verify_email.rs new file mode 100644 index 0000000..d8df8c3 --- /dev/null +++ b/src/api/v1/auth/verify_email.rs @@ -0,0 +1,103 @@ +//! `/api/v1/auth/verify-email` Endpoints for verifying user emails + +use actix_web::{HttpRequest, HttpResponse, get, post, web}; +use chrono::{Duration, Utc}; +use serde::Deserialize; + +use crate::{ + api::v1::auth::check_access_token, error::Error, structs::{EmailToken, Me}, utils::get_auth_header, Data +}; + +#[derive(Deserialize)] +struct Query { + token: String, +} + +/// `GET /api/v1/auth/verify-email` Verifies user email address +/// +/// requires auth? yes +/// +/// ### Query Parameters +/// token +/// +/// ### Responses +/// 200 Success +/// 410 Token Expired +/// 404 Not Found +/// 401 Unauthorized +/// +#[get("/verify-email")] +pub async fn get( + req: HttpRequest, + query: web::Query, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + let me = Me::get(&mut conn, uuid).await?; + + let email_token = EmailToken::get(&mut conn, me.uuid).await?; + + if query.token != email_token.token { + return Ok(HttpResponse::Unauthorized().finish()); + } + + if Utc::now().signed_duration_since(email_token.created_at) > Duration::hours(24) { + email_token.delete(&mut conn).await?; + return Ok(HttpResponse::Gone().finish()); + } + + me.verify_email(&mut conn).await?; + + email_token.delete(&mut conn).await?; + + Ok(HttpResponse::Ok().finish()) +} + +/// `POST /api/v1/auth/verify-email` Sends user verification email +/// +/// requires auth? yes +/// +/// ### Responses +/// 200 Email sent +/// 204 Already verified +/// 429 Too Many Requests +/// 401 Unauthorized +/// +#[post("/verify-email")] +pub async fn post( + req: HttpRequest, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + let me = Me::get(&mut conn, uuid).await?; + + if me.email_verified { + return Ok(HttpResponse::NoContent().finish()) + } + + if let Ok(email_token) = EmailToken::get(&mut conn, me.uuid).await { + if Utc::now().signed_duration_since(email_token.created_at) > Duration::hours(1) { + email_token.delete(&mut conn).await?; + } else { + return Err(Error::TooManyRequests("Please allow 1 hour before sending a new email".to_string())) + } + } + + EmailToken::new(&data, me).await?; + + Ok(HttpResponse::Ok().finish()) +} diff --git a/src/config.rs b/src/config.rs index 4d2e96b..9ffd9c2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,7 +10,7 @@ use url::Url; pub struct ConfigBuilder { database: Database, cache_database: CacheDatabase, - web: Option, + web: WebBuilder, instance: Option, bunny: BunnyBuilder, mail: Mail, @@ -36,8 +36,9 @@ pub struct CacheDatabase { #[derive(Debug, Deserialize)] struct WebBuilder { - url: Option, + ip: Option, port: Option, + url: Url, _ssl: Option, } @@ -57,7 +58,7 @@ struct BunnyBuilder { #[derive(Debug, Deserialize, Clone)] pub struct Mail { pub smtp: Smtp, - pub from: String, + pub address: String, pub tls: String, } @@ -79,16 +80,10 @@ impl ConfigBuilder { } pub fn build(self) -> Config { - let web = if let Some(web) = self.web { - Web { - url: web.url.unwrap_or(String::from("0.0.0.0")), - port: web.port.unwrap_or(8080), - } - } else { - Web { - url: String::from("0.0.0.0"), - port: 8080, - } + let web = Web { + ip: self.web.ip.unwrap_or(String::from("0.0.0.0")), + port: self.web.port.unwrap_or(8080), + url: self.web.url, }; let endpoint = match &*self.bunny.endpoint { @@ -134,8 +129,9 @@ pub struct Config { #[derive(Debug, Clone)] pub struct Web { - pub url: String, + pub ip: String, pub port: u16, + pub url: Url, } #[derive(Debug, Clone)] diff --git a/src/error.rs b/src/error.rs index ca05c98..984f57e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -19,7 +19,7 @@ use serde_json::Error as JsonError; use thiserror::Error; use tokio::task::JoinError; use toml::de::Error as TomlError; -use lettre::{address::AddressError, transport::smtp::Error as SmtpError}; +use lettre::{error::Error as EmailError, address::AddressError, transport::smtp::Error as SmtpError}; #[derive(Debug, Error)] pub enum Error { @@ -56,6 +56,8 @@ pub enum Error { #[error(transparent)] WsClosed(#[from] actix_ws::Closed), #[error(transparent)] + EmailError(#[from] EmailError), + #[error(transparent)] SmtpError(#[from] SmtpError), #[error(transparent)] SmtpAddressError(#[from] AddressError), @@ -68,6 +70,8 @@ pub enum Error { #[error("{0}")] Forbidden(String), #[error("{0}")] + TooManyRequests(String), + #[error("{0}")] InternalServerError(String), } @@ -87,6 +91,8 @@ impl ResponseError for Error { Error::BunnyError(BunnyError::NotFound(_)) => StatusCode::NOT_FOUND, Error::BadRequest(_) => StatusCode::BAD_REQUEST, Error::Unauthorized(_) => StatusCode::UNAUTHORIZED, + Error::Forbidden(_) => StatusCode::FORBIDDEN, + Error::TooManyRequests(_) => StatusCode::TOO_MANY_REQUESTS, _ => StatusCode::INTERNAL_SERVER_ERROR, } } diff --git a/src/main.rs b/src/main.rs index 8bc1c68..0f94be8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -76,7 +76,7 @@ async fn main() -> Result<(), Error> { let mail = config.mail.clone(); - let mail_client = MailClient::new(mail.smtp.credentials(), mail.smtp.server, mail.from, mail.tls)?; + let mail_client = MailClient::new(mail.smtp.credentials(), mail.smtp.server, mail.address, mail.tls)?; let database_url = config.database.url(); @@ -152,7 +152,7 @@ async fn main() -> Result<(), Error> { .wrap(cors) .service(api::web()) }) - .bind((web.url, web.port))? + .bind((web.ip, web.port))? .run() .await?; diff --git a/src/schema.rs b/src/schema.rs index cc5e97c..744ce10 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -31,6 +31,15 @@ diesel::table! { } } +diesel::table! { + email_tokens (token, user_uuid) { + #[max_length = 64] + token -> Varchar, + user_uuid -> Uuid, + created_at -> Timestamptz, + } +} + diesel::table! { guild_members (uuid) { uuid -> Uuid, @@ -133,6 +142,7 @@ diesel::joinable!(access_tokens -> refresh_tokens (refresh_token)); diesel::joinable!(access_tokens -> users (uuid)); diesel::joinable!(channel_permissions -> channels (channel_uuid)); diesel::joinable!(channels -> guilds (guild_uuid)); +diesel::joinable!(email_tokens -> users (user_uuid)); diesel::joinable!(guild_members -> guilds (guild_uuid)); diesel::joinable!(guild_members -> users (user_uuid)); diesel::joinable!(guilds -> users (owner_uuid)); @@ -149,6 +159,7 @@ diesel::allow_tables_to_appear_in_same_query!( access_tokens, channel_permissions, channels, + email_tokens, guild_members, guilds, instance_permissions, diff --git a/src/structs.rs b/src/structs.rs index 541d6ec..50b5ac5 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -1,11 +1,10 @@ use actix_web::web::BytesMut; +use chrono::Utc; use diesel::{ - ExpressionMethods, QueryDsl, Selectable, SelectableHelper, delete, insert_into, - prelude::{Insertable, Queryable}, - update, + delete, dsl::now, insert_into, prelude::{Insertable, Queryable}, update, ExpressionMethods, QueryDsl, Selectable, SelectableHelper }; use diesel_async::{RunQueryDsl, pooled_connection::AsyncDieselConnectionManager}; -use lettre::{message::{Mailbox, MessageBuilder as EmailBuilder}, transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message as Email, Tokio1Executor}; +use lettre::{message::{Mailbox, MessageBuilder as EmailBuilder, MultiPart}, transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message as Email, Tokio1Executor}; use log::debug; use serde::{Deserialize, Serialize}; use tokio::task; @@ -13,10 +12,7 @@ use url::Url; use uuid::Uuid; use crate::{ - Conn, Data, - error::Error, - schema::*, - utils::{image_check, order_by_is_above}, + error::Error, schema::*, utils::{generate_refresh_token, image_check, order_by_is_above}, Conn, Data }; pub trait HasUuid { @@ -70,7 +66,7 @@ impl MailClient { }) } - pub async fn message_builder(&self) -> EmailBuilder { + pub fn message_builder(&self) -> EmailBuilder { Email::builder() .from(self.mbox.clone()) } @@ -780,12 +776,12 @@ impl User { #[diesel(table_name = users)] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct Me { - uuid: Uuid, + pub uuid: Uuid, username: String, display_name: Option, avatar: Option, email: String, - email_verified: bool, + pub email_verified: bool, } impl Me { @@ -849,6 +845,17 @@ impl Me { Ok(()) } + + pub async fn verify_email(&self, conn: &mut Conn) -> Result<(), Error> { + use users::dsl; + update(users::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(dsl::email_verified.eq(true)) + .execute(conn) + .await?; + + Ok(()) + } } #[derive(Deserialize)] @@ -856,3 +863,69 @@ pub struct StartAmountQuery { pub start: Option, pub amount: Option, } + +#[derive(Selectable, Queryable)] +#[diesel(table_name = email_tokens)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct EmailToken { + user_uuid: Uuid, + pub token: String, + pub created_at: chrono::DateTime, +} + +impl EmailToken { + pub async fn get(conn: &mut Conn, user_uuid: Uuid) -> Result { + use email_tokens::dsl; + let email_token = dsl::email_tokens + .filter(dsl::user_uuid.eq(user_uuid)) + .select(EmailToken::as_select()) + .get_result(conn) + .await?; + + Ok(email_token) + } + + pub async fn new(data: &Data, me: Me) -> Result<(), Error> { + let token = generate_refresh_token()?; + + let mut conn = data.pool.get().await?; + + use email_tokens::dsl; + insert_into(email_tokens::table) + .values((dsl::user_uuid.eq(me.uuid), dsl::token.eq(&token), dsl::created_at.eq(now))) + .execute(&mut conn) + .await?; + + let mut verify_endpoint = data.config.web.url.join("verify-email")?; + + verify_endpoint.set_query(Some(&format!("token={}", token))); + + let email = data + .mail_client + .message_builder() + .to(me.email.parse()?) + .subject("Gorb E-mail Verification") + .multipart(MultiPart::alternative_plain_html( + format!("Verify your gorb.app account\n\nHello, {}!\nThanks for creating a new account on Gorb.\nThe final step to create your account is to verify your email address by visiting the page, within 24 hours.\n\n{}\n\nIf you didn't ask to verify this address, you can safely ignore this email\n\nThanks, The gorb team.", me.username, verify_endpoint), + format!(r#"

Verify your {} Account

Hello, {}!

Thanks for creating a new account on Gorb.

The final step to create your account is to verify your email address by clicking the button below, within 24 hours.

VERIFY ACCOUNT

If you didn't ask to verify this address, you can safely ignore this email.

"#, data.config.web.url.domain().unwrap(), me.username, verify_endpoint) + ))?; + + data + .mail_client + .send_mail(email) + .await?; + + Ok(()) + } + + pub async fn delete(&self, conn: &mut Conn) -> Result<(), Error> { + use email_tokens::dsl; + delete(email_tokens::table) + .filter(dsl::user_uuid.eq(self.user_uuid)) + .filter(dsl::token.eq(&self.token)) + .execute(conn) + .await?; + + Ok(()) + } +} From 1ff3fa69a7f5f953ca9fa81ec7d43a5635cb6693 Mon Sep 17 00:00:00 2001 From: Radical Date: Tue, 27 May 2025 22:13:15 +0200 Subject: [PATCH 015/160] ci: automatically create docs --- .woodpecker/publish-docs.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .woodpecker/publish-docs.yml diff --git a/.woodpecker/publish-docs.yml b/.woodpecker/publish-docs.yml new file mode 100644 index 0000000..e6ce482 --- /dev/null +++ b/.woodpecker/publish-docs.yml @@ -0,0 +1,19 @@ +when: + - event: push + branch: main + +steps: + - name: build-docs + image: rust:bookworm + commands: + - cargo doc --release --no-deps + + - name: publish-docs + image: debian:12 + commands: + - apt update -y && apt install -y rsync openssh-client + - printf "Host *\n StrictHostKeyChecking no" >> /etc/ssh/ssh_config + - ssh-agent bash -c "ssh-add <(echo '$KEY' | base64 -d) && rsync --archive --verbose --compress --hard-links --delete-during --partial --progress ./target/doc/ root@gorb.app:/var/www/docs.gorb.app/api && ssh root@gorb.app systemctl reload caddy.service" + environment: + KEY: + from_secret: ssh_key From 9728769b8c71c07ae5d5cf6f5ac4d61676b33217 Mon Sep 17 00:00:00 2001 From: Radical Date: Wed, 28 May 2025 17:36:23 +0200 Subject: [PATCH 016/160] feat: add changing username, email and display_name to /me endpoint --- src/api/v1/auth/login.rs | 6 ++-- src/api/v1/auth/mod.rs | 9 ----- src/api/v1/auth/register.rs | 12 ++++--- src/api/v1/me/mod.rs | 12 +++---- src/structs.rs | 66 +++++++++++++++++++++++++++++++++---- src/utils.rs | 19 ++++++++++- 6 files changed, 95 insertions(+), 29 deletions(-) diff --git a/src/api/v1/auth/login.rs b/src/api/v1/auth/login.rs index c3e8bc7..81ef117 100644 --- a/src/api/v1/auth/login.rs +++ b/src/api/v1/auth/login.rs @@ -9,10 +9,12 @@ use uuid::Uuid; use crate::{ Data, - api::v1::auth::{EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX}, error::Error, schema::*, - utils::{generate_access_token, generate_refresh_token, refresh_token_cookie}, + utils::{ + EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX, generate_access_token, generate_refresh_token, + refresh_token_cookie, + }, }; use super::Response; diff --git a/src/api/v1/auth/mod.rs b/src/api/v1/auth/mod.rs index 2bc0d0b..216e216 100644 --- a/src/api/v1/auth/mod.rs +++ b/src/api/v1/auth/mod.rs @@ -22,15 +22,6 @@ struct Response { access_token: String, } -static EMAIL_REGEX: LazyLock = LazyLock::new(|| { - Regex::new(r"[-A-Za-z0-9!#$%&'*+/=?^_`{|}~]+(?:\.[-A-Za-z0-9!#$%&'*+/=?^_`{|}~]+)*@(?:[A-Za-z0-9](?:[-A-Za-z0-9]*[A-Za-z0-9])?\.)+[A-Za-z0-9](?:[-A-Za-z0-9]*[A-Za-z0-9])?").unwrap() -}); - -static USERNAME_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"^[a-z0-9_.-]+$").unwrap()); - -// Password is expected to be hashed using SHA3-384 -static PASSWORD_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"[0-9a-f]{96}").unwrap()); - pub fn web() -> Scope { web::scope("/auth") .service(register::res) diff --git a/src/api/v1/auth/register.rs b/src/api/v1/auth/register.rs index 66be90a..0e170ee 100644 --- a/src/api/v1/auth/register.rs +++ b/src/api/v1/auth/register.rs @@ -13,14 +13,16 @@ use uuid::Uuid; use super::Response; use crate::{ Data, - api::v1::auth::{EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX}, error::Error, schema::{ access_tokens::{self, dsl as adsl}, refresh_tokens::{self, dsl as rdsl}, users::{self, dsl as udsl}, }, - utils::{generate_access_token, generate_refresh_token, refresh_token_cookie}, + utils::{ + EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX, generate_access_token, generate_refresh_token, + refresh_token_cookie, + }, }; #[derive(Deserialize)] @@ -68,9 +70,11 @@ pub async fn res( data: web::Data, ) -> Result { if !data.config.instance.registration { - return Err(Error::Forbidden("registration is disabled on this instance".to_string())) + return Err(Error::Forbidden( + "registration is disabled on this instance".to_string(), + )); } - + let uuid = Uuid::now_v7(); if !EMAIL_REGEX.is_match(&account_information.email) { diff --git a/src/api/v1/me/mod.rs b/src/api/v1/me/mod.rs index 14067c3..6764875 100644 --- a/src/api/v1/me/mod.rs +++ b/src/api/v1/me/mod.rs @@ -1,5 +1,5 @@ use actix_multipart::form::{MultipartForm, json::Json as MpJson, tempfile::TempFile}; -use actix_web::{get, patch, web, HttpRequest, HttpResponse, Scope}; +use actix_web::{HttpRequest, HttpResponse, Scope, get, patch, web}; use serde::Deserialize; use crate::{ @@ -9,9 +9,7 @@ use crate::{ mod servers; pub fn web() -> Scope { - web::scope("/me") - .service(get) - .service(update) + web::scope("/me").service(get).service(update) } #[get("")] @@ -76,11 +74,11 @@ pub async fn update( if let Some(new_info) = form.json { if let Some(username) = &new_info.username { - todo!(); + me.set_username(&mut conn, username.clone()).await?; } if let Some(display_name) = &new_info.display_name { - todo!(); + me.set_display_name(&mut conn, display_name.clone()).await?; } if let Some(password) = &new_info.password { @@ -88,7 +86,7 @@ pub async fn update( } if let Some(email) = &new_info.email { - todo!(); + me.set_email(&mut conn, email.to_string()).await?; } } diff --git a/src/structs.rs b/src/structs.rs index be4bd43..b5bfa5d 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -14,7 +14,7 @@ use crate::{ Conn, Data, error::Error, schema::*, - utils::{image_check, order_by_is_above}, + utils::{EMAIL_REGEX, USERNAME_REGEX, image_check, order_by_is_above}, }; pub trait HasUuid { @@ -369,11 +369,7 @@ impl Guild { futures::future::try_join_all(guild_futures).await } - pub async fn new( - conn: &mut Conn, - name: String, - owner_uuid: Uuid, - ) -> Result { + pub async fn new(conn: &mut Conn, name: String, owner_uuid: Uuid) -> Result { let guild_uuid = Uuid::now_v7(); let guild_builder = GuildBuilder { @@ -791,6 +787,64 @@ impl Me { Ok(()) } + + pub async fn set_username( + &mut self, + conn: &mut Conn, + new_username: String, + ) -> Result<(), Error> { + if !USERNAME_REGEX.is_match(&new_username) { + return Err(Error::BadRequest("Invalid username".to_string())); + } + + use users::dsl; + update(users::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(dsl::username.eq(new_username.as_str())) + .execute(conn) + .await?; + + self.username = new_username; + + Ok(()) + } + + pub async fn set_display_name( + &mut self, + conn: &mut Conn, + new_display_name: String, + ) -> Result<(), Error> { + use users::dsl; + update(users::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(dsl::display_name.eq(new_display_name.as_str())) + .execute(conn) + .await?; + + self.display_name = Some(new_display_name); + + Ok(()) + } + + pub async fn set_email(&mut self, conn: &mut Conn, new_email: String) -> Result<(), Error> { + if !EMAIL_REGEX.is_match(&new_email) { + return Err(Error::BadRequest("Invalid username".to_string())); + } + + use users::dsl; + update(users::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(( + dsl::email.eq(new_email.as_str()), + dsl::email_verified.eq(false), + )) + .execute(conn) + .await?; + + self.email = new_email; + + Ok(()) + } } #[derive(Deserialize)] diff --git a/src/utils.rs b/src/utils.rs index 3edaab8..143b544 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,3 +1,5 @@ +use std::sync::LazyLock; + use actix_web::{ cookie::{Cookie, SameSite, time::Duration}, http::header::HeaderMap, @@ -7,9 +9,24 @@ use bindet::FileType; use getrandom::fill; use hex::encode; use redis::RedisError; +use regex::Regex; use serde::Serialize; -use crate::{error::Error, structs::{HasIsAbove, HasUuid}, Data}; +use crate::{ + Data, + error::Error, + structs::{HasIsAbove, HasUuid}, +}; + +pub static EMAIL_REGEX: LazyLock = LazyLock::new(|| { + Regex::new(r"[-A-Za-z0-9!#$%&'*+/=?^_`{|}~]+(?:\.[-A-Za-z0-9!#$%&'*+/=?^_`{|}~]+)*@(?:[A-Za-z0-9](?:[-A-Za-z0-9]*[A-Za-z0-9])?\.)+[A-Za-z0-9](?:[-A-Za-z0-9]*[A-Za-z0-9])?").unwrap() +}); + +pub static USERNAME_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r"^[a-z0-9_.-]+$").unwrap()); + +// Password is expected to be hashed using SHA3-384 +pub static PASSWORD_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"[0-9a-f]{96}").unwrap()); pub fn get_auth_header(headers: &HeaderMap) -> Result<&str, Error> { let auth_token = headers.get(actix_web::http::header::AUTHORIZATION); From 501141b584e1ed9627fa6ff9fc3158c0f61de309 Mon Sep 17 00:00:00 2001 From: Radical Date: Wed, 28 May 2025 23:13:41 +0200 Subject: [PATCH 017/160] feat: add password reset --- .../down.sql | 2 + .../up.sql | 7 + src/api/v1/auth/login.rs | 59 ++----- src/api/v1/auth/mod.rs | 9 +- src/api/v1/auth/reset_password.rs | 90 +++++++++++ src/schema.rs | 11 ++ src/structs.rs | 153 +++++++++++++++++- src/utils.rs | 31 +++- 8 files changed, 301 insertions(+), 61 deletions(-) create mode 100644 migrations/2025-05-28-175918_create_password_reset_tokens/down.sql create mode 100644 migrations/2025-05-28-175918_create_password_reset_tokens/up.sql create mode 100644 src/api/v1/auth/reset_password.rs diff --git a/migrations/2025-05-28-175918_create_password_reset_tokens/down.sql b/migrations/2025-05-28-175918_create_password_reset_tokens/down.sql new file mode 100644 index 0000000..dcccc77 --- /dev/null +++ b/migrations/2025-05-28-175918_create_password_reset_tokens/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE password_reset_tokens; diff --git a/migrations/2025-05-28-175918_create_password_reset_tokens/up.sql b/migrations/2025-05-28-175918_create_password_reset_tokens/up.sql new file mode 100644 index 0000000..f788b77 --- /dev/null +++ b/migrations/2025-05-28-175918_create_password_reset_tokens/up.sql @@ -0,0 +1,7 @@ +-- Your SQL goes here +CREATE TABLE password_reset_tokens ( + token VARCHAR(64) NOT NULL, + user_uuid uuid UNIQUE NOT NULL REFERENCES users(uuid), + created_at TIMESTAMPTZ NOT NULL, + PRIMARY KEY (token, user_uuid) +); diff --git a/src/api/v1/auth/login.rs b/src/api/v1/auth/login.rs index 81ef117..254b913 100644 --- a/src/api/v1/auth/login.rs +++ b/src/api/v1/auth/login.rs @@ -5,15 +5,14 @@ use argon2::{PasswordHash, PasswordVerifier}; use diesel::{ExpressionMethods, QueryDsl, dsl::insert_into}; use diesel_async::RunQueryDsl; use serde::Deserialize; -use uuid::Uuid; use crate::{ Data, error::Error, schema::*, utils::{ - EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX, generate_access_token, generate_refresh_token, - refresh_token_cookie, + PASSWORD_REGEX, generate_access_token, generate_refresh_token, + refresh_token_cookie, user_uuid_from_identifier }, }; @@ -39,58 +38,20 @@ pub async fn response( let mut conn = data.pool.get().await?; - if EMAIL_REGEX.is_match(&login_information.username) { - // FIXME: error handling, right now i just want this to work - let (uuid, password): (Uuid, String) = dsl::users - .filter(dsl::email.eq(&login_information.username)) - .select((dsl::uuid, dsl::password)) - .get_result(&mut conn) - .await?; + let uuid = user_uuid_from_identifier(&mut conn, &login_information.username).await?; - return login( - data.clone(), - uuid, - login_information.password.clone(), - password, - login_information.device_name.clone(), - ) - .await; - } else if USERNAME_REGEX.is_match(&login_information.username) { - // FIXME: error handling, right now i just want this to work - let (uuid, password): (Uuid, String) = dsl::users - .filter(dsl::username.eq(&login_information.username)) - .select((dsl::uuid, dsl::password)) - .get_result(&mut conn) - .await?; - - return login( - data.clone(), - uuid, - login_information.password.clone(), - password, - login_information.device_name.clone(), - ) - .await; - } - - Ok(HttpResponse::Unauthorized().finish()) -} - -async fn login( - data: actix_web::web::Data, - uuid: Uuid, - request_password: String, - database_password: String, - device_name: String, -) -> Result { - let mut conn = data.pool.get().await?; + let database_password: String = dsl::users + .filter(dsl::uuid.eq(uuid)) + .select(dsl::password) + .get_result(&mut conn) + .await?; let parsed_hash = PasswordHash::new(&database_password) .map_err(|e| Error::PasswordHashError(e.to_string()))?; if data .argon2 - .verify_password(request_password.as_bytes(), &parsed_hash) + .verify_password(login_information.password.as_bytes(), &parsed_hash) .is_err() { return Err(Error::Unauthorized( @@ -110,7 +71,7 @@ async fn login( rdsl::token.eq(&refresh_token), rdsl::uuid.eq(uuid), rdsl::created_at.eq(current_time), - rdsl::device_name.eq(device_name), + rdsl::device_name.eq(&login_information.device_name), )) .execute(&mut conn) .await?; diff --git a/src/api/v1/auth/mod.rs b/src/api/v1/auth/mod.rs index 1689d2b..cabe114 100644 --- a/src/api/v1/auth/mod.rs +++ b/src/api/v1/auth/mod.rs @@ -1,12 +1,8 @@ -use std::{ - sync::LazyLock, - time::{SystemTime, UNIX_EPOCH}, -}; +use std::time::{SystemTime, UNIX_EPOCH}; use actix_web::{Scope, web}; use diesel::{ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; -use regex::Regex; use serde::Serialize; use uuid::Uuid; @@ -17,6 +13,7 @@ mod refresh; mod register; mod revoke; mod verify_email; +mod reset_password; #[derive(Serialize)] struct Response { @@ -31,6 +28,8 @@ pub fn web() -> Scope { .service(revoke::res) .service(verify_email::get) .service(verify_email::post) + .service(reset_password::get) + .service(reset_password::post) } pub async fn check_access_token(access_token: &str, conn: &mut Conn) -> Result { diff --git a/src/api/v1/auth/reset_password.rs b/src/api/v1/auth/reset_password.rs new file mode 100644 index 0000000..6c6dee7 --- /dev/null +++ b/src/api/v1/auth/reset_password.rs @@ -0,0 +1,90 @@ +//! `/api/v1/auth/reset-password` Endpoints for resetting user password + +use actix_web::{HttpResponse, get, post, web}; +use chrono::{Duration, Utc}; +use serde::Deserialize; + +use crate::{ + error::Error, structs::PasswordResetToken, Data +}; + +#[derive(Deserialize)] +struct Query { + identifier: String, +} + +/// `GET /api/v1/auth/reset-password` Sends password reset email to user +/// +/// requires auth? no +/// +/// ### Query Parameters +/// identifier: Email or username +/// +/// ### Responses +/// 200 Email sent +/// 429 Too Many Requests +/// 404 Not found +/// 400 Bad request +/// +#[get("/reset-password")] +pub async fn get( + query: web::Query, + data: web::Data, +) -> Result { + let mut conn = data.pool.get().await?; + + if let Ok(password_reset_token) = PasswordResetToken::get_with_identifier(&mut conn, query.identifier.clone()).await { + if Utc::now().signed_duration_since(password_reset_token.created_at) > Duration::hours(1) { + password_reset_token.delete(&mut conn).await?; + } else { + return Err(Error::TooManyRequests("Please allow 1 hour before sending a new email".to_string())) + } + } + + PasswordResetToken::new(&data, query.identifier.clone()).await?; + + Ok(HttpResponse::Ok().finish()) +} + +#[derive(Deserialize)] +struct ResetPassword { + password: String, + token: String, +} + +/// `POST /api/v1/auth/reset-password` Resets user password +/// +/// requires auth? no +/// +/// ### Request Example: +/// ``` +/// json!({ +/// "password": "1608c17a27f6ae3891c23d680c73ae91528f20a54dcf4973e2c3126b9734f48b7253047f2395b51bb8a44a6daa188003", +/// "token": "a3f7e29c1b8d0456e2c9f83b7a1d6e4f5028c3b9a7e1f2d5c6b8a0d3e7f4a2b" +/// }); +/// ``` +/// +/// ### Responses +/// 200 Success +/// 410 Token Expired +/// 404 Not Found +/// 400 Bad Request +/// +#[post("/reset-password")] +pub async fn post( + reset_password: web::Json, + data: web::Data, +) -> Result { + let mut conn = data.pool.get().await?; + + let password_reset_token = PasswordResetToken::get(&mut conn, reset_password.token.clone()).await?; + + if Utc::now().signed_duration_since(password_reset_token.created_at) > Duration::hours(24) { + password_reset_token.delete(&mut conn).await?; + return Ok(HttpResponse::Gone().finish()); + } + + password_reset_token.set_password(&data, reset_password.password.clone()).await?; + + Ok(HttpResponse::Ok().finish()) +} diff --git a/src/schema.rs b/src/schema.rs index 744ce10..1b34400 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -89,6 +89,15 @@ diesel::table! { } } +diesel::table! { + password_reset_tokens (token, user_uuid) { + #[max_length = 64] + token -> Varchar, + user_uuid -> Uuid, + created_at -> Timestamptz, + } +} + diesel::table! { refresh_tokens (token) { #[max_length = 64] @@ -151,6 +160,7 @@ diesel::joinable!(invites -> guilds (guild_uuid)); diesel::joinable!(invites -> users (user_uuid)); diesel::joinable!(messages -> channels (channel_uuid)); diesel::joinable!(messages -> users (user_uuid)); +diesel::joinable!(password_reset_tokens -> users (user_uuid)); diesel::joinable!(refresh_tokens -> users (uuid)); diesel::joinable!(role_members -> guild_members (member_uuid)); diesel::joinable!(roles -> guilds (guild_uuid)); @@ -165,6 +175,7 @@ diesel::allow_tables_to_appear_in_same_query!( instance_permissions, invites, messages, + password_reset_tokens, refresh_tokens, role_members, roles, diff --git a/src/structs.rs b/src/structs.rs index e4d2610..e8002bf 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -10,12 +10,13 @@ use serde::{Deserialize, Serialize}; use tokio::task; use url::Url; use uuid::Uuid; +use argon2::{ + PasswordHasher, + password_hash::{SaltString, rand_core::OsRng}, +}; use crate::{ - Conn, Data, - error::Error, - schema::*, - utils::{EMAIL_REGEX, USERNAME_REGEX, generate_refresh_token, image_check, order_by_is_above}, + error::Error, schema::*, utils::{generate_refresh_token, image_check, order_by_is_above, user_uuid_from_identifier, EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX}, Conn, Data }; pub trait HasUuid { @@ -986,3 +987,147 @@ impl EmailToken { Ok(()) } } + +#[derive(Selectable, Queryable)] +#[diesel(table_name = password_reset_tokens)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct PasswordResetToken { + user_uuid: Uuid, + pub token: String, + pub created_at: chrono::DateTime, +} + +impl PasswordResetToken { + pub async fn get(conn: &mut Conn, token: String) -> Result { + use password_reset_tokens::dsl; + let password_reset_token = dsl::password_reset_tokens + .filter(dsl::token.eq(token)) + .select(PasswordResetToken::as_select()) + .get_result(conn) + .await?; + + Ok(password_reset_token) + } + + pub async fn get_with_identifier(conn: &mut Conn, identifier: String) -> Result { + let user_uuid = user_uuid_from_identifier(conn, &identifier).await?; + + use password_reset_tokens::dsl; + let password_reset_token = dsl::password_reset_tokens + .filter(dsl::user_uuid.eq(user_uuid)) + .select(PasswordResetToken::as_select()) + .get_result(conn) + .await?; + + Ok(password_reset_token) + } + + pub async fn new(data: &Data, identifier: String) -> Result<(), Error> { + let token = generate_refresh_token()?; + + let mut conn = data.pool.get().await?; + + let user_uuid = user_uuid_from_identifier(&mut conn, &identifier).await?; + + use users::dsl as udsl; + let (username, email_address): (String, String) = udsl::users + .filter(udsl::uuid.eq(user_uuid)) + .select((udsl::username, udsl::email)) + .get_result(&mut conn) + .await?; + + use password_reset_tokens::dsl; + insert_into(password_reset_tokens::table) + .values((dsl::user_uuid.eq(user_uuid), dsl::token.eq(&token), dsl::created_at.eq(now))) + .execute(&mut conn) + .await?; + + let mut reset_endpoint = data.config.web.url.join("reset-password")?; + + reset_endpoint.set_query(Some(&format!("token={}", token))); + + //TODO: Correct this email + let email = data + .mail_client + .message_builder() + .to(email_address.parse()?) + // twig: change this + .subject("Gorb E-mail Verification") + .multipart(MultiPart::alternative_plain_html( + // twig: change this line + format!("Verify your gorb.app account\n\nHello, {}!\nThanks for creating a new account on Gorb.\nThe final step to create your account is to verify your email address by visiting the page, within 24 hours.\n\n{}\n\nIf you didn't ask to verify this address, you can safely ignore this email\n\nThanks, The gorb team.", username, reset_endpoint), + // twig: and this one + format!(r#"

Verify your {} Account

Hello, {}!

Thanks for creating a new account on Gorb.

The final step to create your account is to verify your email address by clicking the button below, within 24 hours.

VERIFY ACCOUNT

If you didn't ask to verify this address, you can safely ignore this email.

"#, data.config.web.url.domain().unwrap(), username, reset_endpoint) + ))?; + + data + .mail_client + .send_mail(email) + .await?; + + Ok(()) + } + + pub async fn set_password(&self, data: &Data, password: String) -> Result<(), Error> { + if !PASSWORD_REGEX.is_match(&password) { + return Err(Error::BadRequest("Please provide a valid password".to_string())) + } + + let salt = SaltString::generate(&mut OsRng); + + let hashed_password = data + .argon2 + .hash_password(password.as_bytes(), &salt) + .map_err(|e| Error::PasswordHashError(e.to_string()))?; + + let mut conn = data.pool.get().await?; + + use users::dsl; + update(users::table) + .filter(dsl::uuid.eq(self.user_uuid)) + .set(dsl::password.eq(hashed_password.to_string())) + .execute(&mut conn) + .await?; + + let (username, email_address): (String, String) = dsl::users + .filter(dsl::uuid.eq(self.user_uuid)) + .select((dsl::username, dsl::email)) + .get_result(&mut conn) + .await?; + + let login_page = data.config.web.url.join("login")?; + + //TODO: Correct this email + let email = data + .mail_client + .message_builder() + .to(email_address.parse()?) + // twig: change this (post password change email) + .subject("Gorb E-mail Verification") + .multipart(MultiPart::alternative_plain_html( + // twig: change this line + format!("Verify your gorb.app account\n\nHello, {}!\nThanks for creating a new account on Gorb.\nThe final step to create your account is to verify your email address by visiting the page, within 24 hours.\n\n{}\n\nIf you didn't ask to verify this address, you can safely ignore this email\n\nThanks, The gorb team.", username, login_page), + // twig: and this one + format!(r#"

Verify your {} Account

Hello, {}!

Thanks for creating a new account on Gorb.

The final step to create your account is to verify your email address by clicking the button below, within 24 hours.

VERIFY ACCOUNT

If you didn't ask to verify this address, you can safely ignore this email.

"#, data.config.web.url.domain().unwrap(), username, login_page) + ))?; + + data + .mail_client + .send_mail(email) + .await?; + + self.delete(&mut conn).await + } + + pub async fn delete(&self, conn: &mut Conn) -> Result<(), Error> { + use password_reset_tokens::dsl; + delete(password_reset_tokens::table) + .filter(dsl::user_uuid.eq(self.user_uuid)) + .filter(dsl::token.eq(&self.token)) + .execute(conn) + .await?; + + Ok(()) + } +} + diff --git a/src/utils.rs b/src/utils.rs index 143b544..1d39fdb 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -6,16 +6,17 @@ use actix_web::{ web::BytesMut, }; use bindet::FileType; +use diesel::{ExpressionMethods, QueryDsl}; +use diesel_async::RunQueryDsl; use getrandom::fill; use hex::encode; use redis::RedisError; use regex::Regex; use serde::Serialize; +use uuid::Uuid; use crate::{ - Data, - error::Error, - structs::{HasIsAbove, HasUuid}, + error::Error, schema::users, structs::{HasIsAbove, HasUuid}, Conn, Data }; pub static EMAIL_REGEX: LazyLock = LazyLock::new(|| { @@ -136,6 +137,30 @@ pub fn image_check(icon: BytesMut) -> Result { )) } +pub async fn user_uuid_from_identifier(conn: &mut Conn, identifier: &String) -> Result { + if EMAIL_REGEX.is_match(identifier) { + use users::dsl; + let user_uuid = dsl::users + .filter(dsl::email.eq(identifier)) + .select(dsl::uuid) + .get_result(conn) + .await?; + + Ok(user_uuid) + } else if USERNAME_REGEX.is_match(identifier) { + use users::dsl; + let user_uuid = dsl::users + .filter(dsl::username.eq(identifier)) + .select(dsl::uuid) + .get_result(conn) + .await?; + + Ok(user_uuid) + } else { + Err(Error::BadRequest("Please provide a valid username or email".to_string())) + } +} + pub async fn order_by_is_above(mut items: Vec) -> Result, Error> where T: HasUuid + HasIsAbove, From cf2398ed66bf2f148154d9b653d856ad98a63acf Mon Sep 17 00:00:00 2001 From: JustTemmie <47639983+JustTemmie@users.noreply.github.com> Date: Wed, 28 May 2025 23:36:18 +0200 Subject: [PATCH 018/160] fix: fix incorrect email templates --- src/structs.rs | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/structs.rs b/src/structs.rs index e8002bf..019d553 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -962,9 +962,9 @@ impl EmailToken { .mail_client .message_builder() .to(me.email.parse()?) - .subject("Gorb E-mail Verification") + .subject(format!("{} E-mail Verification", data.config.web.url.domain().unwrap())) .multipart(MultiPart::alternative_plain_html( - format!("Verify your gorb.app account\n\nHello, {}!\nThanks for creating a new account on Gorb.\nThe final step to create your account is to verify your email address by visiting the page, within 24 hours.\n\n{}\n\nIf you didn't ask to verify this address, you can safely ignore this email\n\nThanks, The gorb team.", me.username, verify_endpoint), + format!("Verify your {} account\n\nHello, {}!\nThanks for creating a new account on Gorb.\nThe final step to create your account is to verify your email address by visiting the page, within 24 hours.\n\n{}\n\nIf you didn't ask to verify this address, you can safely ignore this email\n\nThanks, The gorb team.", data.config.web.url.domain().unwrap(), me.username, verify_endpoint), format!(r#"

Verify your {} Account

Hello, {}!

Thanks for creating a new account on Gorb.

The final step to create your account is to verify your email address by clicking the button below, within 24 hours.

VERIFY ACCOUNT

If you didn't ask to verify this address, you can safely ignore this email.

"#, data.config.web.url.domain().unwrap(), me.username, verify_endpoint) ))?; @@ -1046,18 +1046,14 @@ impl PasswordResetToken { reset_endpoint.set_query(Some(&format!("token={}", token))); - //TODO: Correct this email let email = data .mail_client .message_builder() .to(email_address.parse()?) - // twig: change this - .subject("Gorb E-mail Verification") + .subject(format!("{} Password Reset", data.config.web.url.domain().unwrap())) .multipart(MultiPart::alternative_plain_html( - // twig: change this line - format!("Verify your gorb.app account\n\nHello, {}!\nThanks for creating a new account on Gorb.\nThe final step to create your account is to verify your email address by visiting the page, within 24 hours.\n\n{}\n\nIf you didn't ask to verify this address, you can safely ignore this email\n\nThanks, The gorb team.", username, reset_endpoint), - // twig: and this one - format!(r#"

Verify your {} Account

Hello, {}!

Thanks for creating a new account on Gorb.

The final step to create your account is to verify your email address by clicking the button below, within 24 hours.

VERIFY ACCOUNT

If you didn't ask to verify this address, you can safely ignore this email.

"#, data.config.web.url.domain().unwrap(), username, reset_endpoint) + format!("{} Password Reset\n\nHello, {}!\nSomeone requested a password reset for your Gorb account.\nClick the button below within 24 hours to reset your password.\n\n{}\n\nIf you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.\n\nThanks, The gorb team.", data.config.web.url.domain().unwrap(), username, reset_endpoint), + format!(r#"

{} Password Reset

Hello, {}!

Someone requested a password reset for your Gorb account.

Click the button below within 24 hours to reset your password.

RESET PASSWORD

If you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.

"#, data.config.web.url.domain().unwrap(), username, reset_endpoint) ))?; data @@ -1097,18 +1093,14 @@ impl PasswordResetToken { let login_page = data.config.web.url.join("login")?; - //TODO: Correct this email let email = data .mail_client .message_builder() .to(email_address.parse()?) - // twig: change this (post password change email) - .subject("Gorb E-mail Verification") + .subject(format!("Your {} Password has been Reset", data.config.web.url.domain().unwrap())) .multipart(MultiPart::alternative_plain_html( - // twig: change this line - format!("Verify your gorb.app account\n\nHello, {}!\nThanks for creating a new account on Gorb.\nThe final step to create your account is to verify your email address by visiting the page, within 24 hours.\n\n{}\n\nIf you didn't ask to verify this address, you can safely ignore this email\n\nThanks, The gorb team.", username, login_page), - // twig: and this one - format!(r#"

Verify your {} Account

Hello, {}!

Thanks for creating a new account on Gorb.

The final step to create your account is to verify your email address by clicking the button below, within 24 hours.

VERIFY ACCOUNT

If you didn't ask to verify this address, you can safely ignore this email.

"#, data.config.web.url.domain().unwrap(), username, login_page) + format!("{} Password Reset Confirmation\n\nHello, {}!\nYour password has been successfully reset for your Gorb account.\nIf you did not initiate this change, please click the link below to reset your password immediately.\n\n{}\n\nThanks, The gorb team.", data.config.web.url.domain().unwrap(), username, login_page), + format!(r#"

{} Password Reset Confirmation

Hello, {}!

Your password has been successfully reset for your Gorb account.

If you did not initiate this change, please click the button below to reset your password immediately.

RESET PASSWORD
"#, data.config.web.url.domain().unwrap(), username, login_page) ))?; data From 65918ae5f24afdb747dead6ca6ea893e26f1bcf3 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 29 May 2025 02:07:36 +0200 Subject: [PATCH 019/160] ci: make build system happy --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8ea076f..3184195 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,10 +30,10 @@ BUNNY_API_KEY=your_storage_zone_password_here \ BUNNY_ENDPOINT=Frankfurt \ BUNNY_ZONE=gorb \ BUNNY_CDN_URL=https://cdn.gorb.app \ -MAIL_ADDRESS=Gorb \ +MAIL_ADDRESS=noreply@gorb.app \ MAIL_TLS=tls \ SMTP_SERVER=mail.gorb.app \ SMTP_USERNAME=your_smtp_username \ -SMTP_PASSWORD=your_smtp_password \ +SMTP_PASSWORD=your_smtp_password ENTRYPOINT ["/usr/bin/entrypoint.sh"] From 4d7aabc8ac1eae7846ff6724c5899042be99212b Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 29 May 2025 02:39:05 +0200 Subject: [PATCH 020/160] feat: include user in message response --- .../v1/servers/uuid/channels/uuid/messages.rs | 34 +++++++++- .../v1/servers/uuid/channels/uuid/socket.rs | 4 +- src/api/v1/servers/uuid/mod.rs | 2 +- src/api/v1/users/uuid.rs | 11 +--- src/structs.rs | 66 +++++++++++++++---- 5 files changed, 90 insertions(+), 27 deletions(-) diff --git a/src/api/v1/servers/uuid/channels/uuid/messages.rs b/src/api/v1/servers/uuid/channels/uuid/messages.rs index 66ec80d..ae96048 100644 --- a/src/api/v1/servers/uuid/channels/uuid/messages.rs +++ b/src/api/v1/servers/uuid/channels/uuid/messages.rs @@ -1,3 +1,5 @@ +//! `/api/v1/servers/{uuid}/channels/{uuid}/messages` Endpoints related to channel messages + use crate::{ Data, api::v1::auth::check_access_token, @@ -15,6 +17,36 @@ struct MessageRequest { offset: i64, } +/// `GET /api/v1/servers/{uuid}/channels/{uuid}/messages` Returns user with the given UUID +/// +/// requires auth: yes +/// +/// requires relation: yes +/// +/// ### Request Example +/// ``` +/// json!({ +/// "amount": 100, +/// "offset": 0 +/// }) +/// ``` +/// +/// ### Response Example +/// ``` +/// json!({ +/// "uuid": "01971976-8618-74c0-b040-7ffbc44823f6", +/// "channel_uuid": "0196fcb1-e886-7de3-b685-0ee46def9a7b", +/// "user_uuid": "0196fc96-a822-76b0-b9bf-a9de232f54b7", +/// "message": "test", +/// "user": { +/// "uuid": "0196fc96-a822-76b0-b9bf-a9de232f54b7", +/// "username": "1234", +/// "display_name": null, +/// "avatar": "https://cdn.gorb.app/avatar/0196fc96-a822-76b0-b9bf-a9de232f54b7/avatar.jpg" +/// } +/// }); +/// ``` +/// #[get("{uuid}/channels/{channel_uuid}/messages")] pub async fn get( req: HttpRequest, @@ -46,7 +78,7 @@ pub async fn get( } let messages = channel - .fetch_messages(&mut conn, message_request.amount, message_request.offset) + .fetch_messages(&data, message_request.amount, message_request.offset) .await?; Ok(HttpResponse::Ok().json(messages)) diff --git a/src/api/v1/servers/uuid/channels/uuid/socket.rs b/src/api/v1/servers/uuid/channels/uuid/socket.rs index a90cb86..03dfdfc 100644 --- a/src/api/v1/servers/uuid/channels/uuid/socket.rs +++ b/src/api/v1/servers/uuid/channels/uuid/socket.rs @@ -15,7 +15,7 @@ use crate::{ }; #[get("{uuid}/channels/{channel_uuid}/socket")] -pub async fn echo( +pub async fn ws( req: HttpRequest, path: web::Path<(Uuid, Uuid)>, stream: web::Payload, @@ -84,7 +84,7 @@ pub async fn echo( let mut conn = data.cache_pool.get_multiplexed_tokio_connection().await?; let message = channel - .new_message(&mut data.pool.get().await?, uuid, text.to_string()) + .new_message(&data, uuid, text.to_string()) .await?; redis::cmd("PUBLISH") diff --git a/src/api/v1/servers/uuid/mod.rs b/src/api/v1/servers/uuid/mod.rs index 80b3068..aa82d11 100644 --- a/src/api/v1/servers/uuid/mod.rs +++ b/src/api/v1/servers/uuid/mod.rs @@ -26,7 +26,7 @@ pub fn web() -> Scope { .service(channels::uuid::get) .service(channels::uuid::delete) .service(channels::uuid::messages::get) - .service(channels::uuid::socket::echo) + .service(channels::uuid::socket::ws) // Roles .service(roles::get) .service(roles::create) diff --git a/src/api/v1/users/uuid.rs b/src/api/v1/users/uuid.rs index 27e3326..1e4b9e5 100644 --- a/src/api/v1/users/uuid.rs +++ b/src/api/v1/users/uuid.rs @@ -39,16 +39,7 @@ pub async fn get( check_access_token(auth_header, &mut conn).await?; - if let Ok(cache_hit) = data.get_cache_key(uuid.to_string()).await { - return Ok(HttpResponse::Ok() - .content_type("application/json") - .body(cache_hit)); - } - - let user = User::fetch_one(&mut conn, uuid).await?; - - data.set_cache_key(uuid.to_string(), user.clone(), 1800) - .await?; + let user = User::fetch_one(&data, uuid).await?; Ok(HttpResponse::Ok().json(user)) } diff --git a/src/structs.rs b/src/structs.rs index 019d553..1efa820 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -263,45 +263,53 @@ impl Channel { pub async fn fetch_messages( &self, - conn: &mut Conn, + data: &Data, amount: i64, offset: i64, ) -> Result, Error> { + let mut conn = data.pool.get().await?; + use messages::dsl; - let messages: Vec = load_or_empty( + let messages: Vec = load_or_empty( dsl::messages .filter(dsl::channel_uuid.eq(self.uuid)) - .select(Message::as_select()) + .select(MessageBuilder::as_select()) .limit(amount) .offset(offset) - .load(conn) + .load(&mut conn) .await, )?; - Ok(messages) + let message_futures = messages.iter().map(async move |b| { + b.build(data).await + }); + + futures::future::try_join_all(message_futures).await } pub async fn new_message( &self, - conn: &mut Conn, + data: &Data, user_uuid: Uuid, message: String, ) -> Result { let message_uuid = Uuid::now_v7(); - let message = Message { + let message = MessageBuilder { uuid: message_uuid, channel_uuid: self.uuid, user_uuid, message, }; + let mut conn = data.pool.get().await?; + insert_into(messages::table) .values(message.clone()) - .execute(conn) + .execute(&mut conn) .await?; - Ok(message) + message.build(data).await } } @@ -697,14 +705,37 @@ impl Member { } } -#[derive(Clone, Serialize, Queryable, Selectable, Insertable)] +#[derive(Clone, Queryable, Selectable, Insertable)] #[diesel(table_name = messages)] #[diesel(check_for_backend(diesel::pg::Pg))] +pub struct MessageBuilder { + uuid: Uuid, + channel_uuid: Uuid, + user_uuid: Uuid, + message: String, +} + +impl MessageBuilder { + pub async fn build(&self, data: &Data) -> Result { + let user = User::fetch_one(data, self.user_uuid).await?; + + Ok(Message { + uuid: self.uuid, + channel_uuid: self.channel_uuid, + user_uuid: self.user_uuid, + message: self.message.clone(), + user, + }) + } +} + +#[derive(Clone, Serialize)] pub struct Message { uuid: Uuid, channel_uuid: Uuid, user_uuid: Uuid, message: String, + user: User, } /// Server invite struct @@ -731,7 +762,7 @@ impl Invite { } } -#[derive(Serialize, Clone, Queryable, Selectable)] +#[derive(Deserialize, Serialize, Clone, Queryable, Selectable)] #[diesel(table_name = users)] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct User { @@ -742,12 +773,21 @@ pub struct User { } impl User { - pub async fn fetch_one(conn: &mut Conn, user_uuid: Uuid) -> Result { + pub async fn fetch_one(data: &Data, user_uuid: Uuid) -> Result { + let mut conn = data.pool.get().await?; + + if let Ok(cache_hit) = data.get_cache_key(user_uuid.to_string()).await { + return Ok(serde_json::from_str(&cache_hit)?); + } + use users::dsl; let user: User = dsl::users .filter(dsl::uuid.eq(user_uuid)) .select(User::as_select()) - .get_result(conn) + .get_result(&mut conn) + .await?; + + data.set_cache_key(user_uuid.to_string(), user.clone(), 1800) .await?; Ok(user) From 21101fecd525e8581ac32290216e68fc1112ec98 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 29 May 2025 03:31:02 +0200 Subject: [PATCH 021/160] build: add missing ca-certificates to docker --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 3184195..bc9fc5d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ RUN cargo build --release FROM debian:12-slim -RUN apt update && apt install libssl3 && rm -rf /var/lib/apt/lists/* /var/cache/apt/* /tmp/* +RUN apt update && apt install libssl3 ca-certificates && rm -rf /var/lib/apt/lists/* /var/cache/apt/* /tmp/* COPY --from=builder /src/target/release/backend /usr/bin/gorb-backend From d0ecf1b375091b1aa8a85e8e8b1dff49e284747d Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 29 May 2025 03:35:39 +0200 Subject: [PATCH 022/160] build: add missing -y flags this shouldnt have built before but i guess libssl3 is included by default? --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index bc9fc5d..a8e8dc6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ RUN cargo build --release FROM debian:12-slim -RUN apt update && apt install libssl3 ca-certificates && rm -rf /var/lib/apt/lists/* /var/cache/apt/* /tmp/* +RUN apt update -y && apt install libssl3 ca-certificates -y && rm -rf /var/lib/apt/lists/* /var/cache/apt/* /tmp/* COPY --from=builder /src/target/release/backend /usr/bin/gorb-backend From d102966198cae443c6d28518f8b81e8c3fcaa415 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 29 May 2025 16:11:13 +0200 Subject: [PATCH 023/160] fix: fetch messages properly --- src/structs.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/structs.rs b/src/structs.rs index 1efa820..eb89980 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -274,6 +274,7 @@ impl Channel { dsl::messages .filter(dsl::channel_uuid.eq(self.uuid)) .select(MessageBuilder::as_select()) + .order(dsl::uuid.desc()) .limit(amount) .offset(offset) .load(&mut conn) From 29dbb085a23b320a5237bf8807c05af42b8799fb Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 29 May 2025 18:31:26 +0200 Subject: [PATCH 024/160] fix: dont require auth to check invite information --- src/api/v1/invites/id.rs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/api/v1/invites/id.rs b/src/api/v1/invites/id.rs index 601d9db..14b444f 100644 --- a/src/api/v1/invites/id.rs +++ b/src/api/v1/invites/id.rs @@ -1,27 +1,16 @@ use actix_web::{HttpRequest, HttpResponse, get, post, web}; use crate::{ - Data, - api::v1::auth::check_access_token, - error::Error, - structs::{Guild, Invite, Member}, - utils::get_auth_header, + api::v1::auth::check_access_token, error::Error, structs::{Guild, Invite, Member}, utils::{get_auth_header, global_checks}, Data }; #[get("{id}")] pub async fn get( - req: HttpRequest, path: web::Path<(String,)>, data: web::Data, ) -> Result { - let headers = req.headers(); - - let auth_header = get_auth_header(headers)?; - let mut conn = data.pool.get().await?; - check_access_token(auth_header, &mut conn).await?; - let invite_id = path.into_inner().0; let invite = Invite::fetch_one(&mut conn, invite_id).await?; From abfbaf8918bcb1942c5dd8c5f0156e36ff2a8747 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 29 May 2025 18:35:13 +0200 Subject: [PATCH 025/160] feat: add global email verification check --- src/api/v1/invites/id.rs | 2 ++ src/api/v1/me/mod.rs | 12 +++++--- src/api/v1/me/servers.rs | 4 ++- src/api/v1/servers/mod.rs | 10 +++---- src/api/v1/servers/uuid/channels/mod.rs | 6 +++- .../v1/servers/uuid/channels/uuid/messages.rs | 8 ++---- src/api/v1/servers/uuid/channels/uuid/mod.rs | 10 +++---- .../v1/servers/uuid/channels/uuid/socket.rs | 7 ++--- src/api/v1/servers/uuid/icon.rs | 8 ++---- src/api/v1/servers/uuid/invites/mod.rs | 10 +++---- src/api/v1/servers/uuid/mod.rs | 8 ++---- src/api/v1/servers/uuid/roles/mod.rs | 4 ++- src/api/v1/servers/uuid/roles/uuid.rs | 8 ++---- src/api/v1/users/mod.rs | 10 +++---- src/api/v1/users/uuid.rs | 10 ++++--- src/config.rs | 28 +++++++++++++++---- src/structs.rs | 4 ++- src/utils.rs | 20 +++++++++++++ 18 files changed, 106 insertions(+), 63 deletions(-) diff --git a/src/api/v1/invites/id.rs b/src/api/v1/invites/id.rs index 14b444f..f7e4a08 100644 --- a/src/api/v1/invites/id.rs +++ b/src/api/v1/invites/id.rs @@ -36,6 +36,8 @@ pub async fn join( let uuid = check_access_token(auth_header, &mut conn).await?; + global_checks(&data, uuid).await?; + let invite = Invite::fetch_one(&mut conn, invite_id).await?; let guild = Guild::fetch_one(&mut conn, invite.guild_uuid).await?; diff --git a/src/api/v1/me/mod.rs b/src/api/v1/me/mod.rs index 605eb1c..cd86e43 100644 --- a/src/api/v1/me/mod.rs +++ b/src/api/v1/me/mod.rs @@ -3,7 +3,7 @@ use actix_web::{HttpRequest, HttpResponse, Scope, get, patch, web}; use serde::Deserialize; use crate::{ - Data, api::v1::auth::check_access_token, error::Error, structs::Me, utils::get_auth_header, + api::v1::auth::check_access_token, error::Error, structs::Me, utils::{get_auth_header, global_checks}, Data }; mod servers; @@ -27,7 +27,7 @@ pub async fn get(req: HttpRequest, data: web::Data) -> Result, display_name: Option, @@ -39,7 +39,7 @@ struct NewInfo { struct UploadForm { #[multipart(limit = "100MB")] avatar: Option, - json: Option>, + json: MpJson>, } #[patch("")] @@ -56,6 +56,10 @@ pub async fn update( let uuid = check_access_token(auth_header, &mut conn).await?; + if form.avatar.is_some() || form.json.0.clone().is_some_and(|ni| ni.username.is_some() || ni.display_name.is_some()) { + global_checks(&data, uuid).await?; + } + let mut me = Me::get(&mut conn, uuid).await?; if let Some(avatar) = form.avatar { @@ -72,7 +76,7 @@ pub async fn update( .await?; } - if let Some(new_info) = form.json { + if let Some(new_info) = form.json.0 { if let Some(username) = &new_info.username { me.set_username(&mut conn, username.clone()).await?; } diff --git a/src/api/v1/me/servers.rs b/src/api/v1/me/servers.rs index 390c31e..ae026a7 100644 --- a/src/api/v1/me/servers.rs +++ b/src/api/v1/me/servers.rs @@ -2,7 +2,7 @@ use actix_web::{get, web, HttpRequest, HttpResponse}; -use crate::{api::v1::auth::check_access_token, error::Error, structs::Me, utils::get_auth_header, Data}; +use crate::{api::v1::auth::check_access_token, error::Error, structs::Me, utils::{get_auth_header, global_checks}, Data}; /// `GET /api/v1/me/servers` Returns all guild memberships in a list @@ -37,6 +37,8 @@ pub async fn get(req: HttpRequest, data: web::Data) -> Result Scope { @@ -87,6 +83,8 @@ pub async fn get( let uuid = check_access_token(auth_header, &mut conn).await?; + global_checks(&data, uuid).await?; + Member::fetch_one(&mut conn, uuid, guild_uuid).await?; let guild = Guild::fetch_one(&mut conn, guild_uuid).await?; diff --git a/src/api/v1/servers/uuid/roles/mod.rs b/src/api/v1/servers/uuid/roles/mod.rs index c0ebaa2..3ae9c5b 100644 --- a/src/api/v1/servers/uuid/roles/mod.rs +++ b/src/api/v1/servers/uuid/roles/mod.rs @@ -3,7 +3,7 @@ use actix_web::{HttpRequest, HttpResponse, get, post, web}; use serde::Deserialize; use crate::{ - api::v1::auth::check_access_token, error::Error, structs::{Member, Role}, utils::{get_auth_header, order_by_is_above}, Data + api::v1::auth::check_access_token, error::Error, structs::{Member, Role}, utils::{get_auth_header, global_checks, order_by_is_above}, Data }; pub mod uuid; @@ -64,6 +64,8 @@ pub async fn create( let uuid = check_access_token(auth_header, &mut conn).await?; + global_checks(&data, uuid).await?; + Member::fetch_one(&mut conn, uuid, guild_uuid).await?; // FIXME: Logic to check permissions, should probably be done in utils.rs diff --git a/src/api/v1/servers/uuid/roles/uuid.rs b/src/api/v1/servers/uuid/roles/uuid.rs index 8ca3cc5..21ab748 100644 --- a/src/api/v1/servers/uuid/roles/uuid.rs +++ b/src/api/v1/servers/uuid/roles/uuid.rs @@ -1,9 +1,5 @@ use crate::{ - Data, - api::v1::auth::check_access_token, - error::Error, - structs::{Member, Role}, - utils::get_auth_header, + api::v1::auth::check_access_token, error::Error, structs::{Member, Role}, utils::{get_auth_header, global_checks}, Data }; use ::uuid::Uuid; use actix_web::{HttpRequest, HttpResponse, get, web}; @@ -24,6 +20,8 @@ pub async fn get( let uuid = check_access_token(auth_header, &mut conn).await?; + global_checks(&data, uuid).await?; + Member::fetch_one(&mut conn, uuid, guild_uuid).await?; if let Ok(cache_hit) = data.get_cache_key(format!("{}", role_uuid)).await { diff --git a/src/api/v1/users/mod.rs b/src/api/v1/users/mod.rs index 0b80fd1..b3d853b 100644 --- a/src/api/v1/users/mod.rs +++ b/src/api/v1/users/mod.rs @@ -3,11 +3,7 @@ use actix_web::{HttpRequest, HttpResponse, Scope, get, web}; use crate::{ - Data, - api::v1::auth::check_access_token, - error::Error, - structs::{StartAmountQuery, User}, - utils::get_auth_header, + api::v1::auth::check_access_token, error::Error, structs::{StartAmountQuery, User}, utils::{get_auth_header, global_checks}, Data }; mod uuid; @@ -68,7 +64,9 @@ pub async fn get( let mut conn = data.pool.get().await?; - check_access_token(auth_header, &mut conn).await?; + let uuid = check_access_token(auth_header, &mut conn).await?; + + global_checks(&data, uuid).await?; let users = User::fetch_amount(&mut conn, start, amount).await?; diff --git a/src/api/v1/users/uuid.rs b/src/api/v1/users/uuid.rs index 1e4b9e5..337019b 100644 --- a/src/api/v1/users/uuid.rs +++ b/src/api/v1/users/uuid.rs @@ -4,7 +4,7 @@ use actix_web::{HttpRequest, HttpResponse, get, web}; use uuid::Uuid; use crate::{ - Data, api::v1::auth::check_access_token, error::Error, structs::User, utils::get_auth_header, + api::v1::auth::check_access_token, error::Error, structs::User, utils::{get_auth_header, global_checks}, Data }; /// `GET /api/v1/users/{uuid}` Returns user with the given UUID @@ -31,15 +31,17 @@ pub async fn get( ) -> Result { let headers = req.headers(); - let uuid = path.into_inner().0; + let user_uuid = path.into_inner().0; let auth_header = get_auth_header(headers)?; let mut conn = data.pool.get().await?; - check_access_token(auth_header, &mut conn).await?; + let uuid = check_access_token(auth_header, &mut conn).await?; - let user = User::fetch_one(&data, uuid).await?; + global_checks(&data, uuid).await?; + + let user = User::fetch_one(&data, user_uuid).await?; Ok(HttpResponse::Ok().json(user)) } diff --git a/src/config.rs b/src/config.rs index 9ffd9c2..6db0483 100644 --- a/src/config.rs +++ b/src/config.rs @@ -11,7 +11,7 @@ pub struct ConfigBuilder { database: Database, cache_database: CacheDatabase, web: WebBuilder, - instance: Option, + instance: Option, bunny: BunnyBuilder, mail: Mail, } @@ -42,9 +42,10 @@ struct WebBuilder { _ssl: Option, } -#[derive(Debug, Deserialize, Clone)] -pub struct Instance { - pub registration: bool, +#[derive(Debug, Deserialize)] +struct InstanceBuilder { + registration: Option, + require_email_verification: Option, } #[derive(Debug, Deserialize)] @@ -106,11 +107,22 @@ impl ConfigBuilder { cdn_url: self.bunny.cdn_url, }; + let instance = match self.instance { + Some(instance) => Instance { + registration: instance.registration.unwrap_or(true), + require_email_verification: instance.require_email_verification.unwrap_or(false), + }, + None => Instance { + registration: true, + require_email_verification: false, + }, + }; + Config { database: self.database, cache_database: self.cache_database, web, - instance: self.instance.unwrap_or(Instance { registration: true }), + instance, bunny, mail: self.mail, } @@ -134,6 +146,12 @@ pub struct Web { pub url: Url, } +#[derive(Debug, Clone)] +pub struct Instance { + pub registration: bool, + pub require_email_verification: bool, +} + #[derive(Debug, Clone)] pub struct Bunny { pub api_key: String, diff --git a/src/structs.rs b/src/structs.rs index eb89980..4ece87a 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -16,7 +16,7 @@ use argon2::{ }; use crate::{ - error::Error, schema::*, utils::{generate_refresh_token, image_check, order_by_is_above, user_uuid_from_identifier, EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX}, Conn, Data + error::Error, schema::*, utils::{generate_refresh_token, global_checks, image_check, order_by_is_above, user_uuid_from_identifier, EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX}, Conn, Data }; pub trait HasUuid { @@ -1070,6 +1070,8 @@ impl PasswordResetToken { let user_uuid = user_uuid_from_identifier(&mut conn, &identifier).await?; + global_checks(&data, user_uuid).await?; + use users::dsl as udsl; let (username, email_address): (String, String) = udsl::users .filter(udsl::uuid.eq(user_uuid)) diff --git a/src/utils.rs b/src/utils.rs index 1d39fdb..d5d4480 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -161,6 +161,26 @@ pub async fn user_uuid_from_identifier(conn: &mut Conn, identifier: &String) -> } } +pub async fn global_checks(data: &Data, user_uuid: Uuid) -> Result<(), Error> { + if data.config.instance.require_email_verification { + let mut conn = data.pool.get().await?; + + use users::dsl; + let email_verified: bool = dsl::users + .filter(dsl::uuid.eq(user_uuid)) + .select(dsl::email_verified) + .get_result(&mut conn) + .await?; + + if !email_verified { + return Err(Error::Forbidden("server requires email verification".to_string())) + } + } + + + Ok(()) +} + pub async fn order_by_is_above(mut items: Vec) -> Result, Error> where T: HasUuid + HasIsAbove, From 8ddcbc49554879052bec94e7149ec66431db93a1 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 29 May 2025 18:36:07 +0200 Subject: [PATCH 026/160] feat: add registration_enabled and email_verification_required fields to stats --- src/api/v1/stats.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/api/v1/stats.rs b/src/api/v1/stats.rs index 4e8aa32..bf2bec3 100644 --- a/src/api/v1/stats.rs +++ b/src/api/v1/stats.rs @@ -18,6 +18,8 @@ struct Response { accounts: i64, uptime: u64, version: String, + registration_enabled: bool, + email_verification_required: bool, build_number: String, } @@ -31,6 +33,8 @@ struct Response { /// "accounts": 3, /// "uptime": 50000, /// "version": "0.1.0", +/// "registration_enabled": true, +/// "email_verification_required": true, /// "build_number": "39d01bb" /// }); /// ``` @@ -50,6 +54,8 @@ pub async fn res(data: web::Data) -> Result { .expect("Seriously why dont you have time??") .as_secs(), version: String::from(VERSION.unwrap_or("UNKNOWN")), + registration_enabled: data.config.instance.registration, + email_verification_required: data.config.instance.require_email_verification, // TODO: Get build number from git hash or remove this from the spec build_number: String::from("how do i implement this?"), }; From 461295c14a0f221e6a4e0469d5a4741f39796705 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 29 May 2025 18:48:29 +0200 Subject: [PATCH 027/160] feat: add instance name and use it in emails --- src/config.rs | 4 ++++ src/structs.rs | 18 +++++++++--------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/config.rs b/src/config.rs index 6db0483..44f73ed 100644 --- a/src/config.rs +++ b/src/config.rs @@ -44,6 +44,7 @@ struct WebBuilder { #[derive(Debug, Deserialize)] struct InstanceBuilder { + name: Option, registration: Option, require_email_verification: Option, } @@ -109,10 +110,12 @@ impl ConfigBuilder { let instance = match self.instance { Some(instance) => Instance { + name: instance.name.unwrap_or("Gorb".to_string()), registration: instance.registration.unwrap_or(true), require_email_verification: instance.require_email_verification.unwrap_or(false), }, None => Instance { + name: "Gorb".to_string(), registration: true, require_email_verification: false, }, @@ -148,6 +151,7 @@ pub struct Web { #[derive(Debug, Clone)] pub struct Instance { + pub name: String, pub registration: bool, pub require_email_verification: bool, } diff --git a/src/structs.rs b/src/structs.rs index 4ece87a..500396f 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -1003,10 +1003,10 @@ impl EmailToken { .mail_client .message_builder() .to(me.email.parse()?) - .subject(format!("{} E-mail Verification", data.config.web.url.domain().unwrap())) + .subject(format!("{} E-mail Verification", data.config.instance.name)) .multipart(MultiPart::alternative_plain_html( - format!("Verify your {} account\n\nHello, {}!\nThanks for creating a new account on Gorb.\nThe final step to create your account is to verify your email address by visiting the page, within 24 hours.\n\n{}\n\nIf you didn't ask to verify this address, you can safely ignore this email\n\nThanks, The gorb team.", data.config.web.url.domain().unwrap(), me.username, verify_endpoint), - format!(r#"

Verify your {} Account

Hello, {}!

Thanks for creating a new account on Gorb.

The final step to create your account is to verify your email address by clicking the button below, within 24 hours.

VERIFY ACCOUNT

If you didn't ask to verify this address, you can safely ignore this email.

"#, data.config.web.url.domain().unwrap(), me.username, verify_endpoint) + format!("Verify your {} account\n\nHello, {}!\nThanks for creating a new account on Gorb.\nThe final step to create your account is to verify your email address by visiting the page, within 24 hours.\n\n{}\n\nIf you didn't ask to verify this address, you can safely ignore this email\n\nThanks, The gorb team.", data.config.instance.name, me.username, verify_endpoint), + format!(r#"

Verify your {} Account

Hello, {}!

Thanks for creating a new account on Gorb.

The final step to create your account is to verify your email address by clicking the button below, within 24 hours.

VERIFY ACCOUNT

If you didn't ask to verify this address, you can safely ignore this email.

"#, data.config.instance.name, me.username, verify_endpoint) ))?; data @@ -1093,10 +1093,10 @@ impl PasswordResetToken { .mail_client .message_builder() .to(email_address.parse()?) - .subject(format!("{} Password Reset", data.config.web.url.domain().unwrap())) + .subject(format!("{} Password Reset", data.config.instance.name)) .multipart(MultiPart::alternative_plain_html( - format!("{} Password Reset\n\nHello, {}!\nSomeone requested a password reset for your Gorb account.\nClick the button below within 24 hours to reset your password.\n\n{}\n\nIf you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.\n\nThanks, The gorb team.", data.config.web.url.domain().unwrap(), username, reset_endpoint), - format!(r#"

{} Password Reset

Hello, {}!

Someone requested a password reset for your Gorb account.

Click the button below within 24 hours to reset your password.

RESET PASSWORD

If you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.

"#, data.config.web.url.domain().unwrap(), username, reset_endpoint) + format!("{} Password Reset\n\nHello, {}!\nSomeone requested a password reset for your Gorb account.\nClick the button below within 24 hours to reset your password.\n\n{}\n\nIf you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.\n\nThanks, The gorb team.", data.config.instance.name, username, reset_endpoint), + format!(r#"

{} Password Reset

Hello, {}!

Someone requested a password reset for your Gorb account.

Click the button below within 24 hours to reset your password.

RESET PASSWORD

If you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.

"#, data.config.instance.name, username, reset_endpoint) ))?; data @@ -1140,10 +1140,10 @@ impl PasswordResetToken { .mail_client .message_builder() .to(email_address.parse()?) - .subject(format!("Your {} Password has been Reset", data.config.web.url.domain().unwrap())) + .subject(format!("Your {} Password has been Reset", data.config.instance.name)) .multipart(MultiPart::alternative_plain_html( - format!("{} Password Reset Confirmation\n\nHello, {}!\nYour password has been successfully reset for your Gorb account.\nIf you did not initiate this change, please click the link below to reset your password immediately.\n\n{}\n\nThanks, The gorb team.", data.config.web.url.domain().unwrap(), username, login_page), - format!(r#"

{} Password Reset Confirmation

Hello, {}!

Your password has been successfully reset for your Gorb account.

If you did not initiate this change, please click the button below to reset your password immediately.

RESET PASSWORD
"#, data.config.web.url.domain().unwrap(), username, login_page) + format!("{} Password Reset Confirmation\n\nHello, {}!\nYour password has been successfully reset for your Gorb account.\nIf you did not initiate this change, please click the link below to reset your password immediately.\n\n{}\n\nThanks, The gorb team.", data.config.instance.name, username, login_page), + format!(r#"

{} Password Reset Confirmation

Hello, {}!

Your password has been successfully reset for your Gorb account.

If you did not initiate this change, please click the button below to reset your password immediately.

RESET PASSWORD
"#, data.config.instance.name, username, login_page) ))?; data From 66c3aef6090087d5a3e64cc322027a29f4c6ad0c Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 29 May 2025 20:11:50 +0200 Subject: [PATCH 028/160] style: move channels to /channels --- src/api/v1/channels/mod.rs | 11 +++ .../uuid => }/channels/uuid/messages.rs | 19 ++--- src/api/v1/channels/uuid/mod.rs | 60 +++++++++++++++ .../uuid => }/channels/uuid/socket.rs | 24 ++---- src/api/v1/mod.rs | 2 + .../uuid/{channels/mod.rs => channels.rs} | 2 - src/api/v1/servers/uuid/channels/uuid/mod.rs | 77 ------------------- src/api/v1/servers/uuid/mod.rs | 4 - src/structs.rs | 33 ++++++-- 9 files changed, 111 insertions(+), 121 deletions(-) create mode 100644 src/api/v1/channels/mod.rs rename src/api/v1/{servers/uuid => }/channels/uuid/messages.rs (76%) create mode 100644 src/api/v1/channels/uuid/mod.rs rename src/api/v1/{servers/uuid => }/channels/uuid/socket.rs (82%) rename src/api/v1/servers/uuid/{channels/mod.rs => channels.rs} (99%) delete mode 100644 src/api/v1/servers/uuid/channels/uuid/mod.rs diff --git a/src/api/v1/channels/mod.rs b/src/api/v1/channels/mod.rs new file mode 100644 index 0000000..d3d5d23 --- /dev/null +++ b/src/api/v1/channels/mod.rs @@ -0,0 +1,11 @@ +use actix_web::{web, Scope}; + +mod uuid; + +pub fn web() -> Scope { + web::scope("/channels") + .service(uuid::get) + .service(uuid::delete) + .service(uuid::messages::get) + .service(uuid::socket::ws) +} diff --git a/src/api/v1/servers/uuid/channels/uuid/messages.rs b/src/api/v1/channels/uuid/messages.rs similarity index 76% rename from src/api/v1/servers/uuid/channels/uuid/messages.rs rename to src/api/v1/channels/uuid/messages.rs index 27bf0cf..25134d2 100644 --- a/src/api/v1/servers/uuid/channels/uuid/messages.rs +++ b/src/api/v1/channels/uuid/messages.rs @@ -43,10 +43,10 @@ struct MessageRequest { /// }); /// ``` /// -#[get("{uuid}/channels/{channel_uuid}/messages")] +#[get("/{uuid}/messages")] pub async fn get( req: HttpRequest, - path: web::Path<(Uuid, Uuid)>, + path: web::Path<(Uuid,)>, message_request: web::Query, data: web::Data, ) -> Result { @@ -54,7 +54,7 @@ pub async fn get( let auth_header = get_auth_header(headers)?; - let (guild_uuid, channel_uuid) = path.into_inner(); + let channel_uuid = path.into_inner().0; let mut conn = data.pool.get().await?; @@ -62,18 +62,9 @@ pub async fn get( global_checks(&data, uuid).await?; - Member::fetch_one(&mut conn, uuid, guild_uuid).await?; + let channel = Channel::fetch_one(&data, channel_uuid).await?; - let channel: Channel; - - if let Ok(cache_hit) = data.get_cache_key(format!("{}", channel_uuid)).await { - channel = serde_json::from_str(&cache_hit)? - } else { - channel = Channel::fetch_one(&mut conn, channel_uuid).await?; - - data.set_cache_key(format!("{}", channel_uuid), channel.clone(), 60) - .await?; - } + Member::fetch_one(&mut conn, uuid, channel.guild_uuid).await?; let messages = channel .fetch_messages(&data, message_request.amount, message_request.offset) diff --git a/src/api/v1/channels/uuid/mod.rs b/src/api/v1/channels/uuid/mod.rs new file mode 100644 index 0000000..f6c93fe --- /dev/null +++ b/src/api/v1/channels/uuid/mod.rs @@ -0,0 +1,60 @@ +pub mod messages; +pub mod socket; + +use crate::{ + api::v1::auth::check_access_token, error::Error, structs::{Channel, Member}, utils::{get_auth_header, global_checks}, Data +}; +use actix_web::{HttpRequest, HttpResponse, delete, get, web}; +use uuid::Uuid; + +#[get("/{uuid}")] +pub async fn get( + req: HttpRequest, + path: web::Path<(Uuid,)>, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let channel_uuid = path.into_inner().0; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + global_checks(&data, uuid).await?; + + let channel = Channel::fetch_one(&data, channel_uuid).await?; + + Member::fetch_one(&mut conn, uuid, channel.guild_uuid).await?; + + Ok(HttpResponse::Ok().json(channel)) +} + +#[delete("/{uuid}")] +pub async fn delete( + req: HttpRequest, + path: web::Path<(Uuid,)>, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let channel_uuid = path.into_inner().0; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + global_checks(&data, uuid).await?; + + let channel = Channel::fetch_one(&data, channel_uuid).await?; + + Member::fetch_one(&mut conn, uuid, channel.guild_uuid).await?; + + channel.delete(&data).await?; + + Ok(HttpResponse::Ok().finish()) +} diff --git a/src/api/v1/servers/uuid/channels/uuid/socket.rs b/src/api/v1/channels/uuid/socket.rs similarity index 82% rename from src/api/v1/servers/uuid/channels/uuid/socket.rs rename to src/api/v1/channels/uuid/socket.rs index 3cbdb8f..c7ca1e8 100644 --- a/src/api/v1/servers/uuid/channels/uuid/socket.rs +++ b/src/api/v1/channels/uuid/socket.rs @@ -11,10 +11,10 @@ use crate::{ api::v1::auth::check_access_token, structs::{Channel, Member}, utils::{get_ws_protocol_header, global_checks}, Data }; -#[get("{uuid}/channels/{channel_uuid}/socket")] +#[get("/{uuid}/socket")] pub async fn ws( req: HttpRequest, - path: web::Path<(Uuid, Uuid)>, + path: web::Path<(Uuid,)>, stream: web::Payload, data: web::Data, ) -> Result { @@ -24,8 +24,8 @@ pub async fn ws( // Retrieve auth header let auth_header = get_ws_protocol_header(headers)?; - // Get uuids from path - let (guild_uuid, channel_uuid) = path.into_inner(); + // Get uuid from path + let channel_uuid = path.into_inner().0; let mut conn = data.pool.get().await.map_err(crate::error::Error::from)?; @@ -34,20 +34,10 @@ pub async fn ws( global_checks(&data, uuid).await?; + let channel = Channel::fetch_one(&data, channel_uuid).await?; + // Get server member from psql - Member::fetch_one(&mut conn, uuid, guild_uuid).await?; - - let channel: Channel; - - // Return channel cache or result from psql as `channel` variable - if let Ok(cache_hit) = data.get_cache_key(format!("{}", channel_uuid)).await { - channel = serde_json::from_str(&cache_hit)? - } else { - channel = Channel::fetch_one(&mut conn, channel_uuid).await?; - - data.set_cache_key(format!("{}", channel_uuid), channel.clone(), 60) - .await?; - } + Member::fetch_one(&mut conn, uuid, channel.guild_uuid).await?; let (mut res, mut session_1, stream) = actix_ws::handle(&req, stream)?; diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs index f30ad58..c08e1e3 100644 --- a/src/api/v1/mod.rs +++ b/src/api/v1/mod.rs @@ -3,6 +3,7 @@ use actix_web::{Scope, web}; mod auth; +mod channels; mod invites; mod servers; mod stats; @@ -14,6 +15,7 @@ pub fn web() -> Scope { .service(stats::res) .service(auth::web()) .service(users::web()) + .service(channels::web()) .service(servers::web()) .service(invites::web()) .service(me::web()) diff --git a/src/api/v1/servers/uuid/channels/mod.rs b/src/api/v1/servers/uuid/channels.rs similarity index 99% rename from src/api/v1/servers/uuid/channels/mod.rs rename to src/api/v1/servers/uuid/channels.rs index 6327e33..813de13 100644 --- a/src/api/v1/servers/uuid/channels/mod.rs +++ b/src/api/v1/servers/uuid/channels.rs @@ -5,8 +5,6 @@ use ::uuid::Uuid; use actix_web::{HttpRequest, HttpResponse, get, post, web}; use serde::Deserialize; -pub mod uuid; - #[derive(Deserialize)] struct ChannelInfo { name: String, diff --git a/src/api/v1/servers/uuid/channels/uuid/mod.rs b/src/api/v1/servers/uuid/channels/uuid/mod.rs deleted file mode 100644 index 946adf3..0000000 --- a/src/api/v1/servers/uuid/channels/uuid/mod.rs +++ /dev/null @@ -1,77 +0,0 @@ -pub mod messages; -pub mod socket; - -use crate::{ - api::v1::auth::check_access_token, error::Error, structs::{Channel, Member}, utils::{get_auth_header, global_checks}, Data -}; -use actix_web::{HttpRequest, HttpResponse, delete, get, web}; -use uuid::Uuid; - -#[get("{uuid}/channels/{channel_uuid}")] -pub async fn get( - req: HttpRequest, - path: web::Path<(Uuid, Uuid)>, - data: web::Data, -) -> Result { - let headers = req.headers(); - - let auth_header = get_auth_header(headers)?; - - let (guild_uuid, channel_uuid) = path.into_inner(); - - let mut conn = data.pool.get().await?; - - let uuid = check_access_token(auth_header, &mut conn).await?; - - global_checks(&data, uuid).await?; - - Member::fetch_one(&mut conn, uuid, guild_uuid).await?; - - if let Ok(cache_hit) = data.get_cache_key(format!("{}", channel_uuid)).await { - return Ok(HttpResponse::Ok() - .content_type("application/json") - .body(cache_hit)); - } - - let channel = Channel::fetch_one(&mut conn, channel_uuid).await?; - - data.set_cache_key(format!("{}", channel_uuid), channel.clone(), 60) - .await?; - - Ok(HttpResponse::Ok().json(channel)) -} - -#[delete("{uuid}/channels/{channel_uuid}")] -pub async fn delete( - req: HttpRequest, - path: web::Path<(Uuid, Uuid)>, - data: web::Data, -) -> Result { - let headers = req.headers(); - - let auth_header = get_auth_header(headers)?; - - let (guild_uuid, channel_uuid) = path.into_inner(); - - let mut conn = data.pool.get().await?; - - let uuid = check_access_token(auth_header, &mut conn).await?; - - global_checks(&data, uuid).await?; - - Member::fetch_one(&mut conn, uuid, guild_uuid).await?; - - let channel: Channel; - - if let Ok(cache_hit) = data.get_cache_key(format!("{}", channel_uuid)).await { - channel = serde_json::from_str(&cache_hit)?; - - data.del_cache_key(format!("{}", channel_uuid)).await?; - } else { - channel = Channel::fetch_one(&mut conn, channel_uuid).await?; - } - - channel.delete(&mut conn).await?; - - Ok(HttpResponse::Ok().finish()) -} diff --git a/src/api/v1/servers/uuid/mod.rs b/src/api/v1/servers/uuid/mod.rs index 39b2925..f874b91 100644 --- a/src/api/v1/servers/uuid/mod.rs +++ b/src/api/v1/servers/uuid/mod.rs @@ -19,10 +19,6 @@ pub fn web() -> Scope { // Channels .service(channels::get) .service(channels::create) - .service(channels::uuid::get) - .service(channels::uuid::delete) - .service(channels::uuid::messages::get) - .service(channels::uuid::socket::ws) // Roles .service(roles::get) .service(roles::create) diff --git a/src/structs.rs b/src/structs.rs index 500396f..8d1d367 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -183,15 +183,26 @@ impl Channel { futures::future::try_join_all(channel_futures).await } - pub async fn fetch_one(conn: &mut Conn, channel_uuid: Uuid) -> Result { + pub async fn fetch_one(data: &Data, channel_uuid: Uuid) -> Result { + if let Ok(cache_hit) = data.get_cache_key(channel_uuid.to_string()).await { + return Ok(serde_json::from_str(&cache_hit)?); + } + + let mut conn = data.pool.get().await?; + use channels::dsl; let channel_builder: ChannelBuilder = dsl::channels .filter(dsl::uuid.eq(channel_uuid)) .select(ChannelBuilder::as_select()) - .get_result(conn) + .get_result(&mut conn) .await?; - channel_builder.build(conn).await + let channel = channel_builder.build(&mut conn).await?; + + data.set_cache_key(channel_uuid.to_string(), channel.clone(), 60) + .await?; + + Ok(channel) } pub async fn new( @@ -245,19 +256,27 @@ impl Channel { data.set_cache_key(channel_uuid.to_string(), channel.clone(), 1800) .await?; - data.del_cache_key(format!("{}_channels", guild_uuid)) - .await?; + if let Ok(_) = data.get_cache_key(format!("{}_channels", guild_uuid)).await { + data.del_cache_key(format!("{}_channels", guild_uuid)) + .await?; + } Ok(channel) } - pub async fn delete(self, conn: &mut Conn) -> Result<(), Error> { + pub async fn delete(self, data: &Data) -> Result<(), Error> { + let mut conn = data.pool.get().await?; + use channels::dsl; delete(channels::table) .filter(dsl::uuid.eq(self.uuid)) - .execute(conn) + .execute(&mut conn) .await?; + if let Ok(_) = data.get_cache_key(self.uuid.to_string()).await { + data.del_cache_key(self.uuid.to_string()).await?; + } + Ok(()) } From 1543a2f4857d0977dd6eb274f730e8675bf55ec7 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 29 May 2025 20:13:01 +0200 Subject: [PATCH 029/160] docs: change path in comments --- src/api/v1/channels/uuid/messages.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/v1/channels/uuid/messages.rs b/src/api/v1/channels/uuid/messages.rs index 25134d2..90055c8 100644 --- a/src/api/v1/channels/uuid/messages.rs +++ b/src/api/v1/channels/uuid/messages.rs @@ -1,4 +1,4 @@ -//! `/api/v1/servers/{uuid}/channels/{uuid}/messages` Endpoints related to channel messages +//! `/api/v1/channels/{uuid}/messages` Endpoints related to channel messages use crate::{ api::v1::auth::check_access_token, error::Error, structs::{Channel, Member}, utils::{get_auth_header, global_checks}, Data @@ -13,7 +13,7 @@ struct MessageRequest { offset: i64, } -/// `GET /api/v1/servers/{uuid}/channels/{uuid}/messages` Returns user with the given UUID +/// `GET /api/v1/channels/{uuid}/messages` Returns user with the given UUID /// /// requires auth: yes /// From e4d9a1b5afe07c8e5d6593acd5c34068c5c43bce Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 29 May 2025 20:15:27 +0200 Subject: [PATCH 030/160] style: move servers to guilds --- src/api/v1/{servers => guilds}/mod.rs | 2 +- src/api/v1/{servers => guilds}/uuid/channels.rs | 0 src/api/v1/{servers => guilds}/uuid/icon.rs | 0 src/api/v1/{servers => guilds}/uuid/invites/id.rs | 0 src/api/v1/{servers => guilds}/uuid/invites/mod.rs | 0 src/api/v1/{servers => guilds}/uuid/mod.rs | 0 src/api/v1/{servers => guilds}/uuid/roles/mod.rs | 0 src/api/v1/{servers => guilds}/uuid/roles/uuid.rs | 0 src/api/v1/mod.rs | 4 ++-- 9 files changed, 3 insertions(+), 3 deletions(-) rename src/api/v1/{servers => guilds}/mod.rs (99%) rename src/api/v1/{servers => guilds}/uuid/channels.rs (100%) rename src/api/v1/{servers => guilds}/uuid/icon.rs (100%) rename src/api/v1/{servers => guilds}/uuid/invites/id.rs (100%) rename src/api/v1/{servers => guilds}/uuid/invites/mod.rs (100%) rename src/api/v1/{servers => guilds}/uuid/mod.rs (100%) rename src/api/v1/{servers => guilds}/uuid/roles/mod.rs (100%) rename src/api/v1/{servers => guilds}/uuid/roles/uuid.rs (100%) diff --git a/src/api/v1/servers/mod.rs b/src/api/v1/guilds/mod.rs similarity index 99% rename from src/api/v1/servers/mod.rs rename to src/api/v1/guilds/mod.rs index f0d27a8..237abce 100644 --- a/src/api/v1/servers/mod.rs +++ b/src/api/v1/guilds/mod.rs @@ -15,7 +15,7 @@ struct GuildInfo { } pub fn web() -> Scope { - web::scope("/servers") + web::scope("/guilds") .service(post) .service(get) .service(uuid::web()) diff --git a/src/api/v1/servers/uuid/channels.rs b/src/api/v1/guilds/uuid/channels.rs similarity index 100% rename from src/api/v1/servers/uuid/channels.rs rename to src/api/v1/guilds/uuid/channels.rs diff --git a/src/api/v1/servers/uuid/icon.rs b/src/api/v1/guilds/uuid/icon.rs similarity index 100% rename from src/api/v1/servers/uuid/icon.rs rename to src/api/v1/guilds/uuid/icon.rs diff --git a/src/api/v1/servers/uuid/invites/id.rs b/src/api/v1/guilds/uuid/invites/id.rs similarity index 100% rename from src/api/v1/servers/uuid/invites/id.rs rename to src/api/v1/guilds/uuid/invites/id.rs diff --git a/src/api/v1/servers/uuid/invites/mod.rs b/src/api/v1/guilds/uuid/invites/mod.rs similarity index 100% rename from src/api/v1/servers/uuid/invites/mod.rs rename to src/api/v1/guilds/uuid/invites/mod.rs diff --git a/src/api/v1/servers/uuid/mod.rs b/src/api/v1/guilds/uuid/mod.rs similarity index 100% rename from src/api/v1/servers/uuid/mod.rs rename to src/api/v1/guilds/uuid/mod.rs diff --git a/src/api/v1/servers/uuid/roles/mod.rs b/src/api/v1/guilds/uuid/roles/mod.rs similarity index 100% rename from src/api/v1/servers/uuid/roles/mod.rs rename to src/api/v1/guilds/uuid/roles/mod.rs diff --git a/src/api/v1/servers/uuid/roles/uuid.rs b/src/api/v1/guilds/uuid/roles/uuid.rs similarity index 100% rename from src/api/v1/servers/uuid/roles/uuid.rs rename to src/api/v1/guilds/uuid/roles/uuid.rs diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs index c08e1e3..421c10d 100644 --- a/src/api/v1/mod.rs +++ b/src/api/v1/mod.rs @@ -5,7 +5,7 @@ use actix_web::{Scope, web}; mod auth; mod channels; mod invites; -mod servers; +mod guilds; mod stats; mod users; mod me; @@ -16,7 +16,7 @@ pub fn web() -> Scope { .service(auth::web()) .service(users::web()) .service(channels::web()) - .service(servers::web()) + .service(guilds::web()) .service(invites::web()) .service(me::web()) } From 556337aa4e13617839e70d24e51ba46fbf9db6b2 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 29 May 2025 20:16:29 +0200 Subject: [PATCH 031/160] docs: fix paths in guild comments --- src/api/v1/guilds/mod.rs | 4 ++-- src/api/v1/guilds/uuid/icon.rs | 4 ++-- src/api/v1/guilds/uuid/mod.rs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/api/v1/guilds/mod.rs b/src/api/v1/guilds/mod.rs index 237abce..4c2dbf1 100644 --- a/src/api/v1/guilds/mod.rs +++ b/src/api/v1/guilds/mod.rs @@ -1,4 +1,4 @@ -//! `/api/v1/servers` Guild related endpoints +//! `/api/v1/guilds` Guild related endpoints use actix_web::{HttpRequest, HttpResponse, Scope, get, post, web}; use serde::Deserialize; @@ -21,7 +21,7 @@ pub fn web() -> Scope { .service(uuid::web()) } -/// `POST /api/v1/servers` Creates a new guild +/// `POST /api/v1/guilds` Creates a new guild /// /// requires auth: yes /// diff --git a/src/api/v1/guilds/uuid/icon.rs b/src/api/v1/guilds/uuid/icon.rs index 876aec4..0ac4470 100644 --- a/src/api/v1/guilds/uuid/icon.rs +++ b/src/api/v1/guilds/uuid/icon.rs @@ -1,4 +1,4 @@ -//! `/api/v1/servers/{uuid}/icon` icon related endpoints, will probably be replaced by a multipart post to above endpoint +//! `/api/v1/guilds/{uuid}/icon` icon related endpoints, will probably be replaced by a multipart post to above endpoint use actix_web::{HttpRequest, HttpResponse, put, web}; use futures_util::StreamExt as _; @@ -8,7 +8,7 @@ use crate::{ api::v1::auth::check_access_token, error::Error, structs::{Guild, Member}, utils::{get_auth_header, global_checks}, Data }; -/// `PUT /api/v1/servers/{uuid}/icon` Icon upload +/// `PUT /api/v1/guilds/{uuid}/icon` Icon upload /// /// requires auth: no /// diff --git a/src/api/v1/guilds/uuid/mod.rs b/src/api/v1/guilds/uuid/mod.rs index f874b91..7ab719d 100644 --- a/src/api/v1/guilds/uuid/mod.rs +++ b/src/api/v1/guilds/uuid/mod.rs @@ -1,4 +1,4 @@ -//! `/api/v1/servers/{uuid}` Specific server endpoints +//! `/api/v1/guilds/{uuid}` Specific server endpoints use actix_web::{HttpRequest, HttpResponse, Scope, get, web}; use uuid::Uuid; @@ -30,7 +30,7 @@ pub fn web() -> Scope { .service(icon::upload) } -/// `GET /api/v1/servers/{uuid}` DESCRIPTION +/// `GET /api/v1/guilds/{uuid}` DESCRIPTION /// /// requires auth: yes /// From 3c5f3fd6544ec418b04dcadec4181b255c1362e9 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 29 May 2025 20:29:45 +0200 Subject: [PATCH 032/160] style: rename url to frontend_url --- Dockerfile | 3 ++- compose.dev.yml | 2 +- compose.yml | 2 +- entrypoint.sh | 3 ++- src/config.rs | 6 +++--- src/structs.rs | 6 +++--- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/Dockerfile b/Dockerfile index a8e8dc6..d7209ef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,8 @@ RUN useradd --create-home --home-dir /gorb gorb USER gorb -ENV WEB_URL=https://gorb.app/web/ \ +ENV WEB_FRONTEND_URL=https://gorb.app/web/ \ +WEB_BASE_PATH=/api \ DATABASE_USERNAME=gorb \ DATABASE_PASSWORD=gorb \ DATABASE=gorb \ diff --git a/compose.dev.yml b/compose.dev.yml index e80f2a7..93a1a85 100644 --- a/compose.dev.yml +++ b/compose.dev.yml @@ -18,7 +18,7 @@ services: - gorb-backend:/gorb environment: #- RUST_LOG=debug - - WEB_URL=https://gorb.app/web/ + - WEB_FRONTEND_URL=https://gorb.app/web/ - DATABASE_USERNAME=gorb - DATABASE_PASSWORD=gorb - DATABASE=gorb diff --git a/compose.yml b/compose.yml index 2bc7339..b1dc07d 100644 --- a/compose.yml +++ b/compose.yml @@ -16,7 +16,7 @@ services: - gorb-backend:/gorb environment: #- RUST_LOG=debug - - WEB_URL=https://gorb.app/web/ + - WEB_FRONTEND_URL=https://gorb.app/web/ - DATABASE_USERNAME=gorb - DATABASE_PASSWORD=gorb - DATABASE=gorb diff --git a/entrypoint.sh b/entrypoint.sh index 9c7a401..38ba890 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -11,7 +11,8 @@ fi if [ ! -f "/gorb/config/config.toml" ]; then cat > /gorb/config/config.toml <, port: Option, - url: Url, + frontend_url: Url, _ssl: Option, } @@ -85,7 +85,7 @@ impl ConfigBuilder { let web = Web { ip: self.web.ip.unwrap_or(String::from("0.0.0.0")), port: self.web.port.unwrap_or(8080), - url: self.web.url, + frontend_url: self.web.frontend_url, }; let endpoint = match &*self.bunny.endpoint { @@ -146,7 +146,7 @@ pub struct Config { pub struct Web { pub ip: String, pub port: u16, - pub url: Url, + pub frontend_url: Url, } #[derive(Debug, Clone)] diff --git a/src/structs.rs b/src/structs.rs index 8d1d367..50615ff 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -1014,7 +1014,7 @@ impl EmailToken { .execute(&mut conn) .await?; - let mut verify_endpoint = data.config.web.url.join("verify-email")?; + let mut verify_endpoint = data.config.web.frontend_url.join("verify-email")?; verify_endpoint.set_query(Some(&format!("token={}", token))); @@ -1104,7 +1104,7 @@ impl PasswordResetToken { .execute(&mut conn) .await?; - let mut reset_endpoint = data.config.web.url.join("reset-password")?; + let mut reset_endpoint = data.config.web.frontend_url.join("reset-password")?; reset_endpoint.set_query(Some(&format!("token={}", token))); @@ -1153,7 +1153,7 @@ impl PasswordResetToken { .get_result(&mut conn) .await?; - let login_page = data.config.web.url.join("login")?; + let login_page = data.config.web.frontend_url.join("login")?; let email = data .mail_client From 94c4428bb0d0eca3e3da1a9ac29148017df6c3ba Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 29 May 2025 20:41:50 +0200 Subject: [PATCH 033/160] feat: add base_path to api Lets you replace /api with whatever you want! --- src/api/mod.rs | 4 ++-- src/config.rs | 3 +++ src/main.rs | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index 87c1c14..4cad2fa 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -6,6 +6,6 @@ use actix_web::web; mod v1; mod versions; -pub fn web() -> Scope { - web::scope("/api").service(v1::web()).service(versions::get) +pub fn web(path: &str) -> Scope { + web::scope(path.trim_end_matches('/')).service(v1::web()).service(versions::get) } diff --git a/src/config.rs b/src/config.rs index 8a274b4..464c98d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -38,6 +38,7 @@ pub struct CacheDatabase { struct WebBuilder { ip: Option, port: Option, + base_path: Option, frontend_url: Url, _ssl: Option, } @@ -85,6 +86,7 @@ impl ConfigBuilder { let web = Web { ip: self.web.ip.unwrap_or(String::from("0.0.0.0")), port: self.web.port.unwrap_or(8080), + base_path: self.web.base_path.unwrap_or("".to_string()), frontend_url: self.web.frontend_url, }; @@ -146,6 +148,7 @@ pub struct Config { pub struct Web { pub ip: String, pub port: u16, + pub base_path: String, pub frontend_url: Url, } diff --git a/src/main.rs b/src/main.rs index 0f94be8..5670e7b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -150,7 +150,7 @@ async fn main() -> Result<(), Error> { App::new() .app_data(web::Data::new(data.clone())) .wrap(cors) - .service(api::web()) + .service(api::web(&data.config.web.base_path)) }) .bind((web.ip, web.port))? .run() From 55e343507e20e2ca5d88915e164249df324e51d1 Mon Sep 17 00:00:00 2001 From: Radical Date: Fri, 30 May 2025 08:37:45 +0000 Subject: [PATCH 034/160] style: move /me/servers to /me/guilds --- src/api/v1/me/{servers.rs => guilds.rs} | 6 +++--- src/api/v1/me/mod.rs | 7 +++++-- 2 files changed, 8 insertions(+), 5 deletions(-) rename src/api/v1/me/{servers.rs => guilds.rs} (89%) diff --git a/src/api/v1/me/servers.rs b/src/api/v1/me/guilds.rs similarity index 89% rename from src/api/v1/me/servers.rs rename to src/api/v1/me/guilds.rs index ae026a7..1e92a34 100644 --- a/src/api/v1/me/servers.rs +++ b/src/api/v1/me/guilds.rs @@ -1,11 +1,11 @@ -//! `/api/v1/me/servers` Contains endpoint related to guild memberships +//! `/api/v1/me/guilds` Contains endpoint related to guild memberships use actix_web::{get, web, HttpRequest, HttpResponse}; use crate::{api::v1::auth::check_access_token, error::Error, structs::Me, utils::{get_auth_header, global_checks}, Data}; -/// `GET /api/v1/me/servers` Returns all guild memberships in a list +/// `GET /api/v1/me/guilds` Returns all guild memberships in a list /// /// requires auth: yes /// @@ -27,7 +27,7 @@ use crate::{api::v1::auth::check_access_token, error::Error, structs::Me, utils: /// ]); /// ``` /// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps -#[get("/servers")] +#[get("/guilds")] pub async fn get(req: HttpRequest, data: web::Data) -> Result { let headers = req.headers(); diff --git a/src/api/v1/me/mod.rs b/src/api/v1/me/mod.rs index cd86e43..ac24342 100644 --- a/src/api/v1/me/mod.rs +++ b/src/api/v1/me/mod.rs @@ -6,10 +6,13 @@ use crate::{ api::v1::auth::check_access_token, error::Error, structs::Me, utils::{get_auth_header, global_checks}, Data }; -mod servers; +mod guilds; pub fn web() -> Scope { - web::scope("/me").service(get).service(update) + web::scope("/me") + .service(get) + .service(update) + .service(guilds::get) } #[get("")] From 746285e0fb09b2b906a5a9f9dc5c009d62e25f6c Mon Sep 17 00:00:00 2001 From: Radical Date: Fri, 30 May 2025 21:11:13 +0200 Subject: [PATCH 035/160] fix: make build number display! --- build.rs | 13 +++++++++++++ src/api/v1/stats.rs | 3 ++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/build.rs b/build.rs index 3a8149e..2bf0bd2 100644 --- a/build.rs +++ b/build.rs @@ -1,3 +1,16 @@ +use std::process::Command; + fn main() { println!("cargo:rerun-if-changed=migrations"); + + let git_short_hash = Command::new("git") + .args(["rev-parse", "--short", "HEAD"]) + .output() + .ok() + .and_then(|o| String::from_utf8(o.stdout).ok()) + .map(|s| s.trim().to_string()) // Trim newline + .unwrap_or_else(|| "UNKNOWN".to_string()); + + // Tell Cargo to set `GIT_SHORT_HASH` for the main compilation + println!("cargo:rustc-env=GIT_SHORT_HASH={}", git_short_hash); } diff --git a/src/api/v1/stats.rs b/src/api/v1/stats.rs index bf2bec3..30888aa 100644 --- a/src/api/v1/stats.rs +++ b/src/api/v1/stats.rs @@ -12,6 +12,7 @@ use crate::error::Error; use crate::schema::users::dsl::{users, uuid}; const VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION"); +const GIT_SHORT_HASH: &str = env!("GIT_SHORT_HASH"); #[derive(Serialize)] struct Response { @@ -57,7 +58,7 @@ pub async fn res(data: web::Data) -> Result { registration_enabled: data.config.instance.registration, email_verification_required: data.config.instance.require_email_verification, // TODO: Get build number from git hash or remove this from the spec - build_number: String::from("how do i implement this?"), + build_number: String::from(GIT_SHORT_HASH), }; Ok(HttpResponse::Ok().json(response)) From c9a3e8c6c41b43524c934bdf43bd0489b1939a51 Mon Sep 17 00:00:00 2001 From: Radical Date: Fri, 30 May 2025 21:12:07 +0200 Subject: [PATCH 036/160] feat: add /guilds/{uuid}members Also makes it return user object with the query --- src/api/v1/channels/uuid/messages.rs | 2 +- src/api/v1/channels/uuid/mod.rs | 4 +- src/api/v1/channels/uuid/socket.rs | 3 +- src/api/v1/guilds/uuid/channels.rs | 4 +- src/api/v1/guilds/uuid/icon.rs | 2 +- src/api/v1/guilds/uuid/invites/mod.rs | 6 +- src/api/v1/guilds/uuid/members.rs | 30 +++++++ src/api/v1/guilds/uuid/mod.rs | 5 +- src/api/v1/guilds/uuid/roles/mod.rs | 4 +- src/api/v1/guilds/uuid/roles/uuid.rs | 2 +- src/api/v1/invites/id.rs | 2 +- src/api/v1/me/guilds.rs | 2 +- src/structs.rs | 117 ++++++++++++++++++++------ 13 files changed, 141 insertions(+), 42 deletions(-) create mode 100644 src/api/v1/guilds/uuid/members.rs diff --git a/src/api/v1/channels/uuid/messages.rs b/src/api/v1/channels/uuid/messages.rs index 90055c8..ea5377e 100644 --- a/src/api/v1/channels/uuid/messages.rs +++ b/src/api/v1/channels/uuid/messages.rs @@ -64,7 +64,7 @@ pub async fn get( let channel = Channel::fetch_one(&data, channel_uuid).await?; - Member::fetch_one(&mut conn, uuid, channel.guild_uuid).await?; + Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; let messages = channel .fetch_messages(&data, message_request.amount, message_request.offset) diff --git a/src/api/v1/channels/uuid/mod.rs b/src/api/v1/channels/uuid/mod.rs index f6c93fe..4e81142 100644 --- a/src/api/v1/channels/uuid/mod.rs +++ b/src/api/v1/channels/uuid/mod.rs @@ -27,7 +27,7 @@ pub async fn get( let channel = Channel::fetch_one(&data, channel_uuid).await?; - Member::fetch_one(&mut conn, uuid, channel.guild_uuid).await?; + Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; Ok(HttpResponse::Ok().json(channel)) } @@ -52,7 +52,7 @@ pub async fn delete( let channel = Channel::fetch_one(&data, channel_uuid).await?; - Member::fetch_one(&mut conn, uuid, channel.guild_uuid).await?; + Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; channel.delete(&data).await?; diff --git a/src/api/v1/channels/uuid/socket.rs b/src/api/v1/channels/uuid/socket.rs index c7ca1e8..34e0fdc 100644 --- a/src/api/v1/channels/uuid/socket.rs +++ b/src/api/v1/channels/uuid/socket.rs @@ -36,8 +36,7 @@ pub async fn ws( let channel = Channel::fetch_one(&data, channel_uuid).await?; - // Get server member from psql - Member::fetch_one(&mut conn, uuid, channel.guild_uuid).await?; + Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; let (mut res, mut session_1, stream) = actix_ws::handle(&req, stream)?; diff --git a/src/api/v1/guilds/uuid/channels.rs b/src/api/v1/guilds/uuid/channels.rs index 813de13..2d01e2b 100644 --- a/src/api/v1/guilds/uuid/channels.rs +++ b/src/api/v1/guilds/uuid/channels.rs @@ -29,7 +29,7 @@ pub async fn get( global_checks(&data, uuid).await?; - Member::fetch_one(&mut conn, uuid, guild_uuid).await?; + Member::check_membership(&mut conn, uuid, guild_uuid).await?; if let Ok(cache_hit) = data.get_cache_key(format!("{}_channels", guild_uuid)).await { return Ok(HttpResponse::Ok() @@ -70,7 +70,7 @@ pub async fn create( global_checks(&data, uuid).await?; - Member::fetch_one(&mut conn, uuid, guild_uuid).await?; + Member::check_membership(&mut conn, uuid, guild_uuid).await?; // FIXME: Logic to check permissions, should probably be done in utils.rs diff --git a/src/api/v1/guilds/uuid/icon.rs b/src/api/v1/guilds/uuid/icon.rs index 0ac4470..ae71321 100644 --- a/src/api/v1/guilds/uuid/icon.rs +++ b/src/api/v1/guilds/uuid/icon.rs @@ -32,7 +32,7 @@ pub async fn upload( global_checks(&data, uuid).await?; - Member::fetch_one(&mut conn, uuid, guild_uuid).await?; + Member::check_membership(&mut conn, uuid, guild_uuid).await?; let mut guild = Guild::fetch_one(&mut conn, guild_uuid).await?; diff --git a/src/api/v1/guilds/uuid/invites/mod.rs b/src/api/v1/guilds/uuid/invites/mod.rs index 83ea08d..e985625 100644 --- a/src/api/v1/guilds/uuid/invites/mod.rs +++ b/src/api/v1/guilds/uuid/invites/mod.rs @@ -29,7 +29,7 @@ pub async fn get( global_checks(&data, uuid).await?; - Member::fetch_one(&mut conn, uuid, guild_uuid).await?; + Member::check_membership(&mut conn, uuid, guild_uuid).await?; let guild = Guild::fetch_one(&mut conn, guild_uuid).await?; @@ -57,13 +57,13 @@ pub async fn create( global_checks(&data, uuid).await?; - let member = Member::fetch_one(&mut conn, uuid, guild_uuid).await?; + Member::check_membership(&mut conn, uuid, guild_uuid).await?; let guild = Guild::fetch_one(&mut conn, guild_uuid).await?; let custom_id = invite_request.as_ref().map(|ir| ir.custom_id.clone()); - let invite = guild.create_invite(&mut conn, &member, custom_id).await?; + let invite = guild.create_invite(&mut conn, uuid, custom_id).await?; Ok(HttpResponse::Ok().json(invite)) } diff --git a/src/api/v1/guilds/uuid/members.rs b/src/api/v1/guilds/uuid/members.rs new file mode 100644 index 0000000..2cc416a --- /dev/null +++ b/src/api/v1/guilds/uuid/members.rs @@ -0,0 +1,30 @@ +use crate::{ + api::v1::auth::check_access_token, error::Error, structs::Member, utils::{get_auth_header, global_checks}, Data +}; +use ::uuid::Uuid; +use actix_web::{HttpRequest, HttpResponse, get, web}; + +#[get("{uuid}/members")] +pub async fn get( + req: HttpRequest, + path: web::Path<(Uuid,)>, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let guild_uuid = path.into_inner().0; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + global_checks(&data, uuid).await?; + + Member::check_membership(&mut conn, uuid, guild_uuid).await?; + + let members = Member::fetch_all(&data, guild_uuid).await?; + + Ok(HttpResponse::Ok().json(members)) +} diff --git a/src/api/v1/guilds/uuid/mod.rs b/src/api/v1/guilds/uuid/mod.rs index 7ab719d..69c3f31 100644 --- a/src/api/v1/guilds/uuid/mod.rs +++ b/src/api/v1/guilds/uuid/mod.rs @@ -7,6 +7,7 @@ mod channels; mod icon; mod invites; mod roles; +mod members; use crate::{ api::v1::auth::check_access_token, error::Error, structs::{Guild, Member}, utils::{get_auth_header, global_checks}, Data @@ -28,6 +29,8 @@ pub fn web() -> Scope { .service(invites::create) // Icon .service(icon::upload) + // Members + .service(members::get) } /// `GET /api/v1/guilds/{uuid}` DESCRIPTION @@ -81,7 +84,7 @@ pub async fn get( global_checks(&data, uuid).await?; - Member::fetch_one(&mut conn, uuid, guild_uuid).await?; + Member::check_membership(&mut conn, uuid, guild_uuid).await?; let guild = Guild::fetch_one(&mut conn, guild_uuid).await?; diff --git a/src/api/v1/guilds/uuid/roles/mod.rs b/src/api/v1/guilds/uuid/roles/mod.rs index 3ae9c5b..9eb6349 100644 --- a/src/api/v1/guilds/uuid/roles/mod.rs +++ b/src/api/v1/guilds/uuid/roles/mod.rs @@ -29,7 +29,7 @@ pub async fn get( let uuid = check_access_token(auth_header, &mut conn).await?; - Member::fetch_one(&mut conn, uuid, guild_uuid).await?; + Member::check_membership(&mut conn, uuid, guild_uuid).await?; if let Ok(cache_hit) = data.get_cache_key(format!("{}_roles", guild_uuid)).await { return Ok(HttpResponse::Ok() @@ -66,7 +66,7 @@ pub async fn create( global_checks(&data, uuid).await?; - Member::fetch_one(&mut conn, uuid, guild_uuid).await?; + Member::check_membership(&mut conn, uuid, guild_uuid).await?; // FIXME: Logic to check permissions, should probably be done in utils.rs diff --git a/src/api/v1/guilds/uuid/roles/uuid.rs b/src/api/v1/guilds/uuid/roles/uuid.rs index 21ab748..9e7853f 100644 --- a/src/api/v1/guilds/uuid/roles/uuid.rs +++ b/src/api/v1/guilds/uuid/roles/uuid.rs @@ -22,7 +22,7 @@ pub async fn get( global_checks(&data, uuid).await?; - Member::fetch_one(&mut conn, uuid, guild_uuid).await?; + Member::check_membership(&mut conn, uuid, guild_uuid).await?; if let Ok(cache_hit) = data.get_cache_key(format!("{}", role_uuid)).await { return Ok(HttpResponse::Ok() diff --git a/src/api/v1/invites/id.rs b/src/api/v1/invites/id.rs index f7e4a08..6ddc53f 100644 --- a/src/api/v1/invites/id.rs +++ b/src/api/v1/invites/id.rs @@ -42,7 +42,7 @@ pub async fn join( let guild = Guild::fetch_one(&mut conn, invite.guild_uuid).await?; - Member::new(&mut conn, uuid, guild.uuid).await?; + Member::new(&data, uuid, guild.uuid).await?; Ok(HttpResponse::Ok().json(guild)) } diff --git a/src/api/v1/me/guilds.rs b/src/api/v1/me/guilds.rs index 1e92a34..516fc35 100644 --- a/src/api/v1/me/guilds.rs +++ b/src/api/v1/me/guilds.rs @@ -41,7 +41,7 @@ pub async fn get(req: HttpRequest, data: web::Data) -> Result, ) -> Result { let invite_id; @@ -530,7 +530,7 @@ impl Guild { let invite = Invite { id: invite_id, - user_uuid: member.user_uuid, + user_uuid, guild_uuid: self.uuid, }; @@ -666,11 +666,34 @@ impl Role { #[derive(Serialize, Queryable, Selectable, Insertable)] #[diesel(table_name = guild_members)] #[diesel(check_for_backend(diesel::pg::Pg))] +pub struct MemberBuilder { + pub uuid: Uuid, + pub nickname: Option, + pub user_uuid: Uuid, + pub guild_uuid: Uuid, +} + +impl MemberBuilder { + async fn build(&self, data: &Data) -> Result { + let user = User::fetch_one(data, self.user_uuid).await?; + + Ok(Member { + uuid: self.uuid, + nickname: self.nickname.clone(), + user_uuid: self.user_uuid, + guild_uuid: self.guild_uuid, + user, + }) + } +} + +#[derive(Serialize, Deserialize)] pub struct Member { pub uuid: Uuid, pub nickname: Option, pub user_uuid: Uuid, pub guild_uuid: Uuid, + user: User, } impl Member { @@ -685,26 +708,61 @@ impl Member { Ok(count) } - pub async fn fetch_one( - conn: &mut Conn, - user_uuid: Uuid, - guild_uuid: Uuid, - ) -> Result { + pub async fn check_membership(conn: &mut Conn, user_uuid: Uuid, guild_uuid: Uuid) -> Result<(), Error> { use guild_members::dsl; - let member: Member = dsl::guild_members + dsl::guild_members .filter(dsl::user_uuid.eq(user_uuid)) .filter(dsl::guild_uuid.eq(guild_uuid)) - .select(Member::as_select()) + .select(MemberBuilder::as_select()) .get_result(conn) .await?; - Ok(member) + Ok(()) } - pub async fn new(conn: &mut Conn, user_uuid: Uuid, guild_uuid: Uuid) -> Result { + pub async fn fetch_one( + data: &Data, + user_uuid: Uuid, + guild_uuid: Uuid, + ) -> Result { + let mut conn = data.pool.get().await?; + + use guild_members::dsl; + let member: MemberBuilder = dsl::guild_members + .filter(dsl::user_uuid.eq(user_uuid)) + .filter(dsl::guild_uuid.eq(guild_uuid)) + .select(MemberBuilder::as_select()) + .get_result(&mut conn) + .await?; + + member.build(data).await + } + + pub async fn fetch_all(data: &Data, guild_uuid: Uuid) -> Result, Error> { + let mut conn = data.pool.get().await?; + + use guild_members::dsl; + let member_builders: Vec = load_or_empty( + dsl::guild_members + .filter(dsl::guild_uuid.eq(guild_uuid)) + .select(MemberBuilder::as_select()) + .load(&mut conn) + .await + )?; + + let member_futures = member_builders.iter().map(async move |m| { + m.build(data).await + }); + + futures::future::try_join_all(member_futures).await + } + + pub async fn new(data: &Data, user_uuid: Uuid, guild_uuid: Uuid) -> Result { + let mut conn = data.pool.get().await?; + let member_uuid = Uuid::now_v7(); - let member = Member { + let member = MemberBuilder { uuid: member_uuid, guild_uuid, user_uuid, @@ -712,16 +770,11 @@ impl Member { }; insert_into(guild_members::table) - .values(member) - .execute(conn) + .values(&member) + .execute(&mut conn) .await?; - Ok(Self { - uuid: member_uuid, - nickname: None, - user_uuid, - guild_uuid, - }) + member.build(data).await } } @@ -856,14 +909,28 @@ impl Me { Ok(me) } - pub async fn fetch_memberships(&self, conn: &mut Conn) -> Result, Error> { + pub async fn fetch_memberships(&self, data: &Data) -> Result, Error> { + let mut conn = data.pool.get().await?; + use guild_members::dsl; - let memberships: Vec = dsl::guild_members + let member_builders: Vec = dsl::guild_members .filter(dsl::user_uuid.eq(self.uuid)) - .select(Member::as_select()) - .load(conn) + .select(MemberBuilder::as_select()) + .load(&mut conn) .await?; + let user = User::fetch_one(data, self.uuid).await?; + + let memberships = member_builders.iter().map(|m| { + Member { + uuid: m.uuid, + nickname: m.nickname.clone(), + user_uuid: m.user_uuid, + guild_uuid: m.guild_uuid, + user: user.clone(), + } + }).collect(); + Ok(memberships) } From d615f1392ea72832e5656580c58a3a29dbd13465 Mon Sep 17 00:00:00 2001 From: Radical Date: Fri, 30 May 2025 21:17:30 +0200 Subject: [PATCH 037/160] style: cargo clippy && cargo fmt --- build.rs | 2 +- src/api/mod.rs | 4 +- src/api/v1/auth/login.rs | 4 +- src/api/v1/auth/mod.rs | 2 +- src/api/v1/auth/refresh.rs | 2 - src/api/v1/auth/reset_password.rs | 40 ++++---- src/api/v1/auth/verify_email.rs | 31 +++--- src/api/v1/channels/mod.rs | 2 +- src/api/v1/channels/uuid/messages.rs | 16 ++-- src/api/v1/channels/uuid/mod.rs | 6 +- src/api/v1/channels/uuid/socket.rs | 9 +- src/api/v1/guilds/mod.rs | 25 +++-- src/api/v1/guilds/uuid/channels.rs | 6 +- src/api/v1/guilds/uuid/icon.rs | 10 +- src/api/v1/guilds/uuid/invites/mod.rs | 6 +- src/api/v1/guilds/uuid/members.rs | 6 +- src/api/v1/guilds/uuid/mod.rs | 14 ++- src/api/v1/guilds/uuid/roles/mod.rs | 6 +- src/api/v1/guilds/uuid/roles/uuid.rs | 6 +- src/api/v1/invites/id.rs | 11 ++- src/api/v1/me/guilds.rs | 17 ++-- src/api/v1/me/mod.rs | 14 ++- src/api/v1/mod.rs | 4 +- src/api/v1/stats.rs | 4 +- src/api/v1/users/mod.rs | 16 ++-- src/api/v1/users/uuid.rs | 12 ++- src/api/versions.rs | 4 +- src/error.rs | 4 +- src/main.rs | 9 +- src/structs.rs | 130 ++++++++++++++++---------- src/utils.rs | 47 ++++++---- 31 files changed, 288 insertions(+), 181 deletions(-) diff --git a/build.rs b/build.rs index 2bf0bd2..45de5ff 100644 --- a/build.rs +++ b/build.rs @@ -8,7 +8,7 @@ fn main() { .output() .ok() .and_then(|o| String::from_utf8(o.stdout).ok()) - .map(|s| s.trim().to_string()) // Trim newline + .map(|s| s.trim().to_string()) // Trim newline .unwrap_or_else(|| "UNKNOWN".to_string()); // Tell Cargo to set `GIT_SHORT_HASH` for the main compilation diff --git a/src/api/mod.rs b/src/api/mod.rs index 4cad2fa..6d83e02 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -7,5 +7,7 @@ mod v1; mod versions; pub fn web(path: &str) -> Scope { - web::scope(path.trim_end_matches('/')).service(v1::web()).service(versions::get) + web::scope(path.trim_end_matches('/')) + .service(v1::web()) + .service(versions::get) } diff --git a/src/api/v1/auth/login.rs b/src/api/v1/auth/login.rs index 254b913..5229425 100644 --- a/src/api/v1/auth/login.rs +++ b/src/api/v1/auth/login.rs @@ -11,8 +11,8 @@ use crate::{ error::Error, schema::*, utils::{ - PASSWORD_REGEX, generate_access_token, generate_refresh_token, - refresh_token_cookie, user_uuid_from_identifier + PASSWORD_REGEX, generate_access_token, generate_refresh_token, refresh_token_cookie, + user_uuid_from_identifier, }, }; diff --git a/src/api/v1/auth/mod.rs b/src/api/v1/auth/mod.rs index cabe114..d627a59 100644 --- a/src/api/v1/auth/mod.rs +++ b/src/api/v1/auth/mod.rs @@ -11,9 +11,9 @@ use crate::{Conn, error::Error, schema::access_tokens::dsl}; mod login; mod refresh; mod register; +mod reset_password; mod revoke; mod verify_email; -mod reset_password; #[derive(Serialize)] struct Response { diff --git a/src/api/v1/auth/refresh.rs b/src/api/v1/auth/refresh.rs index fceabf5..b64b10e 100644 --- a/src/api/v1/auth/refresh.rs +++ b/src/api/v1/auth/refresh.rs @@ -61,8 +61,6 @@ pub async fn res(req: HttpRequest, data: web::Data) -> Result 1987200 { let new_refresh_token = generate_refresh_token()?; - let new_refresh_token = new_refresh_token; - match update(refresh_tokens::table) .filter(rdsl::token.eq(&refresh_token)) .set(( diff --git a/src/api/v1/auth/reset_password.rs b/src/api/v1/auth/reset_password.rs index 6c6dee7..8240fbd 100644 --- a/src/api/v1/auth/reset_password.rs +++ b/src/api/v1/auth/reset_password.rs @@ -4,9 +4,7 @@ use actix_web::{HttpResponse, get, post, web}; use chrono::{Duration, Utc}; use serde::Deserialize; -use crate::{ - error::Error, structs::PasswordResetToken, Data -}; +use crate::{Data, error::Error, structs::PasswordResetToken}; #[derive(Deserialize)] struct Query { @@ -14,30 +12,31 @@ struct Query { } /// `GET /api/v1/auth/reset-password` Sends password reset email to user -/// +/// /// requires auth? no -/// +/// /// ### Query Parameters /// identifier: Email or username -/// +/// /// ### Responses /// 200 Email sent /// 429 Too Many Requests /// 404 Not found /// 400 Bad request -/// +/// #[get("/reset-password")] -pub async fn get( - query: web::Query, - data: web::Data, -) -> Result { +pub async fn get(query: web::Query, data: web::Data) -> Result { let mut conn = data.pool.get().await?; - if let Ok(password_reset_token) = PasswordResetToken::get_with_identifier(&mut conn, query.identifier.clone()).await { + if let Ok(password_reset_token) = + PasswordResetToken::get_with_identifier(&mut conn, query.identifier.clone()).await + { if Utc::now().signed_duration_since(password_reset_token.created_at) > Duration::hours(1) { password_reset_token.delete(&mut conn).await?; } else { - return Err(Error::TooManyRequests("Please allow 1 hour before sending a new email".to_string())) + return Err(Error::TooManyRequests( + "Please allow 1 hour before sending a new email".to_string(), + )); } } @@ -53,9 +52,9 @@ struct ResetPassword { } /// `POST /api/v1/auth/reset-password` Resets user password -/// +/// /// requires auth? no -/// +/// /// ### Request Example: /// ``` /// json!({ @@ -63,13 +62,13 @@ struct ResetPassword { /// "token": "a3f7e29c1b8d0456e2c9f83b7a1d6e4f5028c3b9a7e1f2d5c6b8a0d3e7f4a2b" /// }); /// ``` -/// +/// /// ### Responses /// 200 Success /// 410 Token Expired /// 404 Not Found /// 400 Bad Request -/// +/// #[post("/reset-password")] pub async fn post( reset_password: web::Json, @@ -77,14 +76,17 @@ pub async fn post( ) -> Result { let mut conn = data.pool.get().await?; - let password_reset_token = PasswordResetToken::get(&mut conn, reset_password.token.clone()).await?; + let password_reset_token = + PasswordResetToken::get(&mut conn, reset_password.token.clone()).await?; if Utc::now().signed_duration_since(password_reset_token.created_at) > Duration::hours(24) { password_reset_token.delete(&mut conn).await?; return Ok(HttpResponse::Gone().finish()); } - password_reset_token.set_password(&data, reset_password.password.clone()).await?; + password_reset_token + .set_password(&data, reset_password.password.clone()) + .await?; Ok(HttpResponse::Ok().finish()) } diff --git a/src/api/v1/auth/verify_email.rs b/src/api/v1/auth/verify_email.rs index d8df8c3..c5c9097 100644 --- a/src/api/v1/auth/verify_email.rs +++ b/src/api/v1/auth/verify_email.rs @@ -5,7 +5,11 @@ use chrono::{Duration, Utc}; use serde::Deserialize; use crate::{ - api::v1::auth::check_access_token, error::Error, structs::{EmailToken, Me}, utils::get_auth_header, Data + Data, + api::v1::auth::check_access_token, + error::Error, + structs::{EmailToken, Me}, + utils::get_auth_header, }; #[derive(Deserialize)] @@ -14,18 +18,18 @@ struct Query { } /// `GET /api/v1/auth/verify-email` Verifies user email address -/// +/// /// requires auth? yes -/// +/// /// ### Query Parameters /// token -/// +/// /// ### Responses /// 200 Success /// 410 Token Expired /// 404 Not Found /// 401 Unauthorized -/// +/// #[get("/verify-email")] pub async fn get( req: HttpRequest, @@ -61,20 +65,17 @@ pub async fn get( } /// `POST /api/v1/auth/verify-email` Sends user verification email -/// +/// /// requires auth? yes -/// +/// /// ### Responses /// 200 Email sent /// 204 Already verified /// 429 Too Many Requests /// 401 Unauthorized -/// +/// #[post("/verify-email")] -pub async fn post( - req: HttpRequest, - data: web::Data, -) -> Result { +pub async fn post(req: HttpRequest, data: web::Data) -> Result { let headers = req.headers(); let auth_header = get_auth_header(headers)?; @@ -86,14 +87,16 @@ pub async fn post( let me = Me::get(&mut conn, uuid).await?; if me.email_verified { - return Ok(HttpResponse::NoContent().finish()) + return Ok(HttpResponse::NoContent().finish()); } if let Ok(email_token) = EmailToken::get(&mut conn, me.uuid).await { if Utc::now().signed_duration_since(email_token.created_at) > Duration::hours(1) { email_token.delete(&mut conn).await?; } else { - return Err(Error::TooManyRequests("Please allow 1 hour before sending a new email".to_string())) + return Err(Error::TooManyRequests( + "Please allow 1 hour before sending a new email".to_string(), + )); } } diff --git a/src/api/v1/channels/mod.rs b/src/api/v1/channels/mod.rs index d3d5d23..999bb23 100644 --- a/src/api/v1/channels/mod.rs +++ b/src/api/v1/channels/mod.rs @@ -1,4 +1,4 @@ -use actix_web::{web, Scope}; +use actix_web::{Scope, web}; mod uuid; diff --git a/src/api/v1/channels/uuid/messages.rs b/src/api/v1/channels/uuid/messages.rs index ea5377e..ddcc800 100644 --- a/src/api/v1/channels/uuid/messages.rs +++ b/src/api/v1/channels/uuid/messages.rs @@ -1,7 +1,11 @@ //! `/api/v1/channels/{uuid}/messages` Endpoints related to channel messages use crate::{ - api::v1::auth::check_access_token, error::Error, structs::{Channel, Member}, utils::{get_auth_header, global_checks}, Data + Data, + api::v1::auth::check_access_token, + error::Error, + structs::{Channel, Member}, + utils::{get_auth_header, global_checks}, }; use ::uuid::Uuid; use actix_web::{HttpRequest, HttpResponse, get, web}; @@ -14,11 +18,11 @@ struct MessageRequest { } /// `GET /api/v1/channels/{uuid}/messages` Returns user with the given UUID -/// +/// /// requires auth: yes -/// +/// /// requires relation: yes -/// +/// /// ### Request Example /// ``` /// json!({ @@ -26,7 +30,7 @@ struct MessageRequest { /// "offset": 0 /// }) /// ``` -/// +/// /// ### Response Example /// ``` /// json!({ @@ -42,7 +46,7 @@ struct MessageRequest { /// } /// }); /// ``` -/// +/// #[get("/{uuid}/messages")] pub async fn get( req: HttpRequest, diff --git a/src/api/v1/channels/uuid/mod.rs b/src/api/v1/channels/uuid/mod.rs index 4e81142..f429159 100644 --- a/src/api/v1/channels/uuid/mod.rs +++ b/src/api/v1/channels/uuid/mod.rs @@ -2,7 +2,11 @@ pub mod messages; pub mod socket; use crate::{ - api::v1::auth::check_access_token, error::Error, structs::{Channel, Member}, utils::{get_auth_header, global_checks}, Data + Data, + api::v1::auth::check_access_token, + error::Error, + structs::{Channel, Member}, + utils::{get_auth_header, global_checks}, }; use actix_web::{HttpRequest, HttpResponse, delete, get, web}; use uuid::Uuid; diff --git a/src/api/v1/channels/uuid/socket.rs b/src/api/v1/channels/uuid/socket.rs index 34e0fdc..556dca3 100644 --- a/src/api/v1/channels/uuid/socket.rs +++ b/src/api/v1/channels/uuid/socket.rs @@ -8,7 +8,10 @@ use futures_util::StreamExt as _; use uuid::Uuid; use crate::{ - api::v1::auth::check_access_token, structs::{Channel, Member}, utils::{get_ws_protocol_header, global_checks}, Data + Data, + api::v1::auth::check_access_token, + structs::{Channel, Member}, + utils::{get_ws_protocol_header, global_checks}, }; #[get("/{uuid}/socket")] @@ -71,9 +74,7 @@ pub async fn ws( Ok(AggregatedMessage::Text(text)) => { let mut conn = data.cache_pool.get_multiplexed_tokio_connection().await?; - let message = channel - .new_message(&data, uuid, text.to_string()) - .await?; + let message = channel.new_message(&data, uuid, text.to_string()).await?; redis::cmd("PUBLISH") .arg(&[channel_uuid.to_string(), serde_json::to_string(&message)?]) diff --git a/src/api/v1/guilds/mod.rs b/src/api/v1/guilds/mod.rs index 4c2dbf1..b7a7a7c 100644 --- a/src/api/v1/guilds/mod.rs +++ b/src/api/v1/guilds/mod.rs @@ -6,7 +6,11 @@ use serde::Deserialize; mod uuid; use crate::{ - api::v1::auth::check_access_token, error::Error, structs::{Guild, StartAmountQuery}, utils::{get_auth_header, global_checks}, Data + Data, + api::v1::auth::check_access_token, + error::Error, + structs::{Guild, StartAmountQuery}, + utils::{get_auth_header, global_checks}, }; #[derive(Deserialize)] @@ -22,16 +26,16 @@ pub fn web() -> Scope { } /// `POST /api/v1/guilds` Creates a new guild -/// +/// /// requires auth: yes -/// +/// /// ### Request Example /// ``` /// json!({ /// "name": "My new server!" /// }); /// ``` -/// +/// /// ### Response Example /// ``` /// json!({ @@ -59,22 +63,17 @@ pub async fn post( let uuid = check_access_token(auth_header, &mut conn).await?; - let guild = Guild::new( - &mut conn, - guild_info.name.clone(), - uuid, - ) - .await?; + let guild = Guild::new(&mut conn, guild_info.name.clone(), uuid).await?; Ok(HttpResponse::Ok().json(guild)) } /// `GET /api/v1/servers` Fetches all guilds -/// +/// /// requires auth: yes -/// +/// /// requires admin: yes -/// +/// /// ### Response Example /// ``` /// json!([ diff --git a/src/api/v1/guilds/uuid/channels.rs b/src/api/v1/guilds/uuid/channels.rs index 2d01e2b..0dd4566 100644 --- a/src/api/v1/guilds/uuid/channels.rs +++ b/src/api/v1/guilds/uuid/channels.rs @@ -1,5 +1,9 @@ use crate::{ - api::v1::auth::check_access_token, error::Error, structs::{Channel, Member}, utils::{get_auth_header, global_checks, order_by_is_above}, Data + Data, + api::v1::auth::check_access_token, + error::Error, + structs::{Channel, Member}, + utils::{get_auth_header, global_checks, order_by_is_above}, }; use ::uuid::Uuid; use actix_web::{HttpRequest, HttpResponse, get, post, web}; diff --git a/src/api/v1/guilds/uuid/icon.rs b/src/api/v1/guilds/uuid/icon.rs index ae71321..f2e15b6 100644 --- a/src/api/v1/guilds/uuid/icon.rs +++ b/src/api/v1/guilds/uuid/icon.rs @@ -5,13 +5,17 @@ use futures_util::StreamExt as _; use uuid::Uuid; use crate::{ - api::v1::auth::check_access_token, error::Error, structs::{Guild, Member}, utils::{get_auth_header, global_checks}, Data + Data, + api::v1::auth::check_access_token, + error::Error, + structs::{Guild, Member}, + utils::{get_auth_header, global_checks}, }; /// `PUT /api/v1/guilds/{uuid}/icon` Icon upload -/// +/// /// requires auth: no -/// +/// /// put request expects a file and nothing else #[put("{uuid}/icon")] pub async fn upload( diff --git a/src/api/v1/guilds/uuid/invites/mod.rs b/src/api/v1/guilds/uuid/invites/mod.rs index e985625..ea04529 100644 --- a/src/api/v1/guilds/uuid/invites/mod.rs +++ b/src/api/v1/guilds/uuid/invites/mod.rs @@ -3,7 +3,11 @@ use serde::Deserialize; use uuid::Uuid; use crate::{ - api::v1::auth::check_access_token, error::Error, structs::{Guild, Member}, utils::{get_auth_header, global_checks}, Data + Data, + api::v1::auth::check_access_token, + error::Error, + structs::{Guild, Member}, + utils::{get_auth_header, global_checks}, }; #[derive(Deserialize)] diff --git a/src/api/v1/guilds/uuid/members.rs b/src/api/v1/guilds/uuid/members.rs index 2cc416a..d7ed0a5 100644 --- a/src/api/v1/guilds/uuid/members.rs +++ b/src/api/v1/guilds/uuid/members.rs @@ -1,5 +1,9 @@ use crate::{ - api::v1::auth::check_access_token, error::Error, structs::Member, utils::{get_auth_header, global_checks}, Data + Data, + api::v1::auth::check_access_token, + error::Error, + structs::Member, + utils::{get_auth_header, global_checks}, }; use ::uuid::Uuid; use actix_web::{HttpRequest, HttpResponse, get, web}; diff --git a/src/api/v1/guilds/uuid/mod.rs b/src/api/v1/guilds/uuid/mod.rs index 69c3f31..c24e957 100644 --- a/src/api/v1/guilds/uuid/mod.rs +++ b/src/api/v1/guilds/uuid/mod.rs @@ -6,11 +6,15 @@ use uuid::Uuid; mod channels; mod icon; mod invites; -mod roles; mod members; +mod roles; use crate::{ - api::v1::auth::check_access_token, error::Error, structs::{Guild, Member}, utils::{get_auth_header, global_checks}, Data + Data, + api::v1::auth::check_access_token, + error::Error, + structs::{Guild, Member}, + utils::{get_auth_header, global_checks}, }; pub fn web() -> Scope { @@ -34,9 +38,9 @@ pub fn web() -> Scope { } /// `GET /api/v1/guilds/{uuid}` DESCRIPTION -/// +/// /// requires auth: yes -/// +/// /// ### Response Example /// ``` /// json!({ @@ -65,7 +69,7 @@ pub fn web() -> Scope { /// ], /// "member_count": 20 /// }); -/// ``` +/// ``` #[get("/{uuid}")] pub async fn get( req: HttpRequest, diff --git a/src/api/v1/guilds/uuid/roles/mod.rs b/src/api/v1/guilds/uuid/roles/mod.rs index 9eb6349..8015384 100644 --- a/src/api/v1/guilds/uuid/roles/mod.rs +++ b/src/api/v1/guilds/uuid/roles/mod.rs @@ -3,7 +3,11 @@ use actix_web::{HttpRequest, HttpResponse, get, post, web}; use serde::Deserialize; use crate::{ - api::v1::auth::check_access_token, error::Error, structs::{Member, Role}, utils::{get_auth_header, global_checks, order_by_is_above}, Data + Data, + api::v1::auth::check_access_token, + error::Error, + structs::{Member, Role}, + utils::{get_auth_header, global_checks, order_by_is_above}, }; pub mod uuid; diff --git a/src/api/v1/guilds/uuid/roles/uuid.rs b/src/api/v1/guilds/uuid/roles/uuid.rs index 9e7853f..0e7f306 100644 --- a/src/api/v1/guilds/uuid/roles/uuid.rs +++ b/src/api/v1/guilds/uuid/roles/uuid.rs @@ -1,5 +1,9 @@ use crate::{ - api::v1::auth::check_access_token, error::Error, structs::{Member, Role}, utils::{get_auth_header, global_checks}, Data + Data, + api::v1::auth::check_access_token, + error::Error, + structs::{Member, Role}, + utils::{get_auth_header, global_checks}, }; use ::uuid::Uuid; use actix_web::{HttpRequest, HttpResponse, get, web}; diff --git a/src/api/v1/invites/id.rs b/src/api/v1/invites/id.rs index 6ddc53f..687b825 100644 --- a/src/api/v1/invites/id.rs +++ b/src/api/v1/invites/id.rs @@ -1,14 +1,15 @@ use actix_web::{HttpRequest, HttpResponse, get, post, web}; use crate::{ - api::v1::auth::check_access_token, error::Error, structs::{Guild, Invite, Member}, utils::{get_auth_header, global_checks}, Data + Data, + api::v1::auth::check_access_token, + error::Error, + structs::{Guild, Invite, Member}, + utils::{get_auth_header, global_checks}, }; #[get("{id}")] -pub async fn get( - path: web::Path<(String,)>, - data: web::Data, -) -> Result { +pub async fn get(path: web::Path<(String,)>, data: web::Data) -> Result { let mut conn = data.pool.get().await?; let invite_id = path.into_inner().0; diff --git a/src/api/v1/me/guilds.rs b/src/api/v1/me/guilds.rs index 516fc35..06c3328 100644 --- a/src/api/v1/me/guilds.rs +++ b/src/api/v1/me/guilds.rs @@ -1,14 +1,19 @@ //! `/api/v1/me/guilds` Contains endpoint related to guild memberships -use actix_web::{get, web, HttpRequest, HttpResponse}; - -use crate::{api::v1::auth::check_access_token, error::Error, structs::Me, utils::{get_auth_header, global_checks}, Data}; +use actix_web::{HttpRequest, HttpResponse, get, web}; +use crate::{ + Data, + api::v1::auth::check_access_token, + error::Error, + structs::Me, + utils::{get_auth_header, global_checks}, +}; /// `GET /api/v1/me/guilds` Returns all guild memberships in a list -/// +/// /// requires auth: yes -/// +/// /// ### Example Response /// ``` /// json!([ @@ -44,4 +49,4 @@ pub async fn get(req: HttpRequest, data: web::Data) -> Result Scope { web::scope("/v1") diff --git a/src/api/v1/stats.rs b/src/api/v1/stats.rs index 30888aa..5877c00 100644 --- a/src/api/v1/stats.rs +++ b/src/api/v1/stats.rs @@ -25,9 +25,9 @@ struct Response { } /// `GET /api/v1/` Returns stats about the server -/// +/// /// requires auth: no -/// +/// /// ### Response Example /// ``` /// json!({ diff --git a/src/api/v1/users/mod.rs b/src/api/v1/users/mod.rs index b3d853b..fd3980d 100644 --- a/src/api/v1/users/mod.rs +++ b/src/api/v1/users/mod.rs @@ -3,23 +3,25 @@ use actix_web::{HttpRequest, HttpResponse, Scope, get, web}; use crate::{ - api::v1::auth::check_access_token, error::Error, structs::{StartAmountQuery, User}, utils::{get_auth_header, global_checks}, Data + Data, + api::v1::auth::check_access_token, + error::Error, + structs::{StartAmountQuery, User}, + utils::{get_auth_header, global_checks}, }; mod uuid; pub fn web() -> Scope { - web::scope("/users") - .service(get) - .service(uuid::get) + web::scope("/users").service(get).service(uuid::get) } /// `GET /api/v1/users` Returns all users on this instance -/// +/// /// requires auth: yes -/// +/// /// requires admin: yes -/// +/// /// ### Response Example /// ``` /// json!([ diff --git a/src/api/v1/users/uuid.rs b/src/api/v1/users/uuid.rs index 337019b..213afe5 100644 --- a/src/api/v1/users/uuid.rs +++ b/src/api/v1/users/uuid.rs @@ -4,15 +4,19 @@ use actix_web::{HttpRequest, HttpResponse, get, web}; use uuid::Uuid; use crate::{ - api::v1::auth::check_access_token, error::Error, structs::User, utils::{get_auth_header, global_checks}, Data + Data, + api::v1::auth::check_access_token, + error::Error, + structs::User, + utils::{get_auth_header, global_checks}, }; /// `GET /api/v1/users/{uuid}` Returns user with the given UUID -/// +/// /// requires auth: yes -/// +/// /// requires relation: yes -/// +/// /// ### Response Example /// ``` /// json!({ diff --git a/src/api/versions.rs b/src/api/versions.rs index 809d6ed..0c3e106 100644 --- a/src/api/versions.rs +++ b/src/api/versions.rs @@ -12,9 +12,9 @@ struct Response { struct UnstableFeatures; /// `GET /api/versions` Returns info about api versions. -/// +/// /// requires auth: no -/// +/// /// ### Response Example /// ``` /// json!({ diff --git a/src/error.rs b/src/error.rs index 984f57e..1b1bfba 100644 --- a/src/error.rs +++ b/src/error.rs @@ -12,6 +12,9 @@ use bunny_api_tokio::error::Error as BunnyError; use deadpool::managed::{BuildError, PoolError}; use diesel::{ConnectionError, result::Error as DieselError}; use diesel_async::pooled_connection::PoolError as DieselPoolError; +use lettre::{ + address::AddressError, error::Error as EmailError, transport::smtp::Error as SmtpError, +}; use log::{debug, error}; use redis::RedisError; use serde::Serialize; @@ -19,7 +22,6 @@ use serde_json::Error as JsonError; use thiserror::Error; use tokio::task::JoinError; use toml::de::Error as TomlError; -use lettre::{error::Error as EmailError, address::AddressError, transport::smtp::Error as SmtpError}; #[derive(Debug, Error)] pub enum Error { diff --git a/src/main.rs b/src/main.rs index 5670e7b..892c79d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,8 +6,8 @@ use diesel_async::pooled_connection::AsyncDieselConnectionManager; use diesel_async::pooled_connection::deadpool::Pool; use error::Error; use simple_logger::SimpleLogger; -use structs::MailClient; use std::time::SystemTime; +use structs::MailClient; mod config; use config::{Config, ConfigBuilder}; use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations}; @@ -76,7 +76,12 @@ async fn main() -> Result<(), Error> { let mail = config.mail.clone(); - let mail_client = MailClient::new(mail.smtp.credentials(), mail.smtp.server, mail.address, mail.tls)?; + let mail_client = MailClient::new( + mail.smtp.credentials(), + mail.smtp.server, + mail.address, + mail.tls, + )?; let database_url = config.database.url(); diff --git a/src/structs.rs b/src/structs.rs index bf49a30..e76aa2a 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -1,22 +1,36 @@ use actix_web::web::BytesMut; +use argon2::{ + PasswordHasher, + password_hash::{SaltString, rand_core::OsRng}, +}; use chrono::Utc; use diesel::{ - delete, dsl::now, insert_into, prelude::{Insertable, Queryable}, update, ExpressionMethods, QueryDsl, Selectable, SelectableHelper + ExpressionMethods, QueryDsl, Selectable, SelectableHelper, delete, + dsl::now, + insert_into, + prelude::{Insertable, Queryable}, + update, }; use diesel_async::{RunQueryDsl, pooled_connection::AsyncDieselConnectionManager}; -use lettre::{message::{Mailbox, MessageBuilder as EmailBuilder, MultiPart}, transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message as Email, Tokio1Executor}; +use lettre::{ + AsyncSmtpTransport, AsyncTransport, Message as Email, Tokio1Executor, + message::{Mailbox, MessageBuilder as EmailBuilder, MultiPart}, + transport::smtp::authentication::Credentials, +}; use log::debug; use serde::{Deserialize, Serialize}; use tokio::task; use url::Url; use uuid::Uuid; -use argon2::{ - PasswordHasher, - password_hash::{SaltString, rand_core::OsRng}, -}; use crate::{ - error::Error, schema::*, utils::{generate_refresh_token, global_checks, image_check, order_by_is_above, user_uuid_from_identifier, EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX}, Conn, Data + Conn, Data, + error::Error, + schema::*, + utils::{ + EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX, generate_refresh_token, global_checks, + image_check, order_by_is_above, user_uuid_from_identifier, + }, }; pub trait HasUuid { @@ -61,7 +75,12 @@ pub struct MailClient { } impl MailClient { - pub fn new>(creds: Credentials, smtp_server: String, mbox: String, tls: T) -> Result { + pub fn new>( + creds: Credentials, + smtp_server: String, + mbox: String, + tls: T, + ) -> Result { Ok(Self { creds, smtp_server, @@ -71,15 +90,16 @@ impl MailClient { } pub fn message_builder(&self) -> EmailBuilder { - Email::builder() - .from(self.mbox.clone()) + Email::builder().from(self.mbox.clone()) } pub async fn send_mail(&self, email: Email) -> Result<(), Error> { let mailer: AsyncSmtpTransport = match self.tls { - MailTls::StartTls => AsyncSmtpTransport::::starttls_relay(&self.smtp_server)? - .credentials(self.creds.clone()) - .build(), + MailTls::StartTls => { + AsyncSmtpTransport::::starttls_relay(&self.smtp_server)? + .credentials(self.creds.clone()) + .build() + } MailTls::Tls => AsyncSmtpTransport::::relay(&self.smtp_server)? .credentials(self.creds.clone()) .build(), @@ -256,7 +276,11 @@ impl Channel { data.set_cache_key(channel_uuid.to_string(), channel.clone(), 1800) .await?; - if let Ok(_) = data.get_cache_key(format!("{}_channels", guild_uuid)).await { + if data + .get_cache_key(format!("{}_channels", guild_uuid)) + .await + .is_ok() + { data.del_cache_key(format!("{}_channels", guild_uuid)) .await?; } @@ -273,7 +297,7 @@ impl Channel { .execute(&mut conn) .await?; - if let Ok(_) = data.get_cache_key(self.uuid.to_string()).await { + if data.get_cache_key(self.uuid.to_string()).await.is_ok() { data.del_cache_key(self.uuid.to_string()).await?; } @@ -300,9 +324,7 @@ impl Channel { .await, )?; - let message_futures = messages.iter().map(async move |b| { - b.build(data).await - }); + let message_futures = messages.iter().map(async move |b| b.build(data).await); futures::future::try_join_all(message_futures).await } @@ -708,7 +730,11 @@ impl Member { Ok(count) } - pub async fn check_membership(conn: &mut Conn, user_uuid: Uuid, guild_uuid: Uuid) -> Result<(), Error> { + pub async fn check_membership( + conn: &mut Conn, + user_uuid: Uuid, + guild_uuid: Uuid, + ) -> Result<(), Error> { use guild_members::dsl; dsl::guild_members .filter(dsl::user_uuid.eq(user_uuid)) @@ -720,11 +746,7 @@ impl Member { Ok(()) } - pub async fn fetch_one( - data: &Data, - user_uuid: Uuid, - guild_uuid: Uuid, - ) -> Result { + pub async fn fetch_one(data: &Data, user_uuid: Uuid, guild_uuid: Uuid) -> Result { let mut conn = data.pool.get().await?; use guild_members::dsl; @@ -747,12 +769,12 @@ impl Member { .filter(dsl::guild_uuid.eq(guild_uuid)) .select(MemberBuilder::as_select()) .load(&mut conn) - .await + .await, )?; - let member_futures = member_builders.iter().map(async move |m| { - m.build(data).await - }); + let member_futures = member_builders + .iter() + .map(async move |m| m.build(data).await); futures::future::try_join_all(member_futures).await } @@ -921,15 +943,16 @@ impl Me { let user = User::fetch_one(data, self.uuid).await?; - let memberships = member_builders.iter().map(|m| { - Member { + let memberships = member_builders + .iter() + .map(|m| Member { uuid: m.uuid, nickname: m.nickname.clone(), user_uuid: m.user_uuid, guild_uuid: m.guild_uuid, user: user.clone(), - } - }).collect(); + }) + .collect(); Ok(memberships) } @@ -1070,6 +1093,7 @@ impl EmailToken { Ok(email_token) } + #[allow(clippy::new_ret_no_self)] pub async fn new(data: &Data, me: Me) -> Result<(), Error> { let token = generate_refresh_token()?; @@ -1077,7 +1101,11 @@ impl EmailToken { use email_tokens::dsl; insert_into(email_tokens::table) - .values((dsl::user_uuid.eq(me.uuid), dsl::token.eq(&token), dsl::created_at.eq(now))) + .values(( + dsl::user_uuid.eq(me.uuid), + dsl::token.eq(&token), + dsl::created_at.eq(now), + )) .execute(&mut conn) .await?; @@ -1095,10 +1123,7 @@ impl EmailToken { format!(r#"

Verify your {} Account

Hello, {}!

Thanks for creating a new account on Gorb.

The final step to create your account is to verify your email address by clicking the button below, within 24 hours.

VERIFY ACCOUNT

If you didn't ask to verify this address, you can safely ignore this email.

"#, data.config.instance.name, me.username, verify_endpoint) ))?; - data - .mail_client - .send_mail(email) - .await?; + data.mail_client.send_mail(email).await?; Ok(()) } @@ -1136,7 +1161,10 @@ impl PasswordResetToken { Ok(password_reset_token) } - pub async fn get_with_identifier(conn: &mut Conn, identifier: String) -> Result { + pub async fn get_with_identifier( + conn: &mut Conn, + identifier: String, + ) -> Result { let user_uuid = user_uuid_from_identifier(conn, &identifier).await?; use password_reset_tokens::dsl; @@ -1149,6 +1177,7 @@ impl PasswordResetToken { Ok(password_reset_token) } + #[allow(clippy::new_ret_no_self)] pub async fn new(data: &Data, identifier: String) -> Result<(), Error> { let token = generate_refresh_token()?; @@ -1156,7 +1185,7 @@ impl PasswordResetToken { let user_uuid = user_uuid_from_identifier(&mut conn, &identifier).await?; - global_checks(&data, user_uuid).await?; + global_checks(data, user_uuid).await?; use users::dsl as udsl; let (username, email_address): (String, String) = udsl::users @@ -1167,7 +1196,11 @@ impl PasswordResetToken { use password_reset_tokens::dsl; insert_into(password_reset_tokens::table) - .values((dsl::user_uuid.eq(user_uuid), dsl::token.eq(&token), dsl::created_at.eq(now))) + .values(( + dsl::user_uuid.eq(user_uuid), + dsl::token.eq(&token), + dsl::created_at.eq(now), + )) .execute(&mut conn) .await?; @@ -1185,17 +1218,16 @@ impl PasswordResetToken { format!(r#"

{} Password Reset

Hello, {}!

Someone requested a password reset for your Gorb account.

Click the button below within 24 hours to reset your password.

RESET PASSWORD

If you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.

"#, data.config.instance.name, username, reset_endpoint) ))?; - data - .mail_client - .send_mail(email) - .await?; + data.mail_client.send_mail(email).await?; Ok(()) } pub async fn set_password(&self, data: &Data, password: String) -> Result<(), Error> { if !PASSWORD_REGEX.is_match(&password) { - return Err(Error::BadRequest("Please provide a valid password".to_string())) + return Err(Error::BadRequest( + "Please provide a valid password".to_string(), + )); } let salt = SaltString::generate(&mut OsRng); @@ -1204,7 +1236,7 @@ impl PasswordResetToken { .argon2 .hash_password(password.as_bytes(), &salt) .map_err(|e| Error::PasswordHashError(e.to_string()))?; - + let mut conn = data.pool.get().await?; use users::dsl; @@ -1232,10 +1264,7 @@ impl PasswordResetToken { format!(r#"

{} Password Reset Confirmation

Hello, {}!

Your password has been successfully reset for your Gorb account.

If you did not initiate this change, please click the button below to reset your password immediately.

RESET PASSWORD
"#, data.config.instance.name, username, login_page) ))?; - data - .mail_client - .send_mail(email) - .await?; + data.mail_client.send_mail(email).await?; self.delete(&mut conn).await } @@ -1251,4 +1280,3 @@ impl PasswordResetToken { Ok(()) } } - diff --git a/src/utils.rs b/src/utils.rs index d5d4480..5ef2187 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -16,7 +16,10 @@ use serde::Serialize; use uuid::Uuid; use crate::{ - error::Error, schema::users, structs::{HasIsAbove, HasUuid}, Conn, Data + Conn, Data, + error::Error, + schema::users, + structs::{HasIsAbove, HasUuid}, }; pub static EMAIL_REGEX: LazyLock = LazyLock::new(|| { @@ -137,27 +140,32 @@ pub fn image_check(icon: BytesMut) -> Result { )) } -pub async fn user_uuid_from_identifier(conn: &mut Conn, identifier: &String) -> Result { +pub async fn user_uuid_from_identifier( + conn: &mut Conn, + identifier: &String, +) -> Result { if EMAIL_REGEX.is_match(identifier) { - use users::dsl; - let user_uuid = dsl::users - .filter(dsl::email.eq(identifier)) - .select(dsl::uuid) - .get_result(conn) - .await?; + use users::dsl; + let user_uuid = dsl::users + .filter(dsl::email.eq(identifier)) + .select(dsl::uuid) + .get_result(conn) + .await?; - Ok(user_uuid) + Ok(user_uuid) } else if USERNAME_REGEX.is_match(identifier) { - use users::dsl; - let user_uuid = dsl::users - .filter(dsl::username.eq(identifier)) - .select(dsl::uuid) - .get_result(conn) - .await?; + use users::dsl; + let user_uuid = dsl::users + .filter(dsl::username.eq(identifier)) + .select(dsl::uuid) + .get_result(conn) + .await?; - Ok(user_uuid) + Ok(user_uuid) } else { - Err(Error::BadRequest("Please provide a valid username or email".to_string())) + Err(Error::BadRequest( + "Please provide a valid username or email".to_string(), + )) } } @@ -173,11 +181,12 @@ pub async fn global_checks(data: &Data, user_uuid: Uuid) -> Result<(), Error> { .await?; if !email_verified { - return Err(Error::Forbidden("server requires email verification".to_string())) + return Err(Error::Forbidden( + "server requires email verification".to_string(), + )); } } - Ok(()) } From 38aab46534e823a9d33b33b23924405e2d5f58ca Mon Sep 17 00:00:00 2001 From: Radical Date: Sat, 31 May 2025 14:41:29 +0200 Subject: [PATCH 038/160] style: rename refresh_token_cookie() to new_refresh_token_cookie() and fix error message when no refresh_token is found on refresh --- src/api/v1/auth/login.rs | 4 ++-- src/api/v1/auth/refresh.rs | 16 ++++------------ src/api/v1/auth/register.rs | 4 ++-- src/utils.rs | 2 +- 4 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/api/v1/auth/login.rs b/src/api/v1/auth/login.rs index 5229425..04d6b4f 100644 --- a/src/api/v1/auth/login.rs +++ b/src/api/v1/auth/login.rs @@ -11,7 +11,7 @@ use crate::{ error::Error, schema::*, utils::{ - PASSWORD_REGEX, generate_access_token, generate_refresh_token, refresh_token_cookie, + PASSWORD_REGEX, generate_access_token, generate_refresh_token, new_refresh_token_cookie, user_uuid_from_identifier, }, }; @@ -89,6 +89,6 @@ pub async fn response( .await?; Ok(HttpResponse::Ok() - .cookie(refresh_token_cookie(refresh_token)) + .cookie(new_refresh_token_cookie(refresh_token)) .json(Response { access_token })) } diff --git a/src/api/v1/auth/refresh.rs b/src/api/v1/auth/refresh.rs index b64b10e..cc3bbe9 100644 --- a/src/api/v1/auth/refresh.rs +++ b/src/api/v1/auth/refresh.rs @@ -11,20 +11,16 @@ use crate::{ access_tokens::{self, dsl}, refresh_tokens::{self, dsl as rdsl}, }, - utils::{generate_access_token, generate_refresh_token, refresh_token_cookie}, + utils::{generate_access_token, generate_refresh_token, new_refresh_token_cookie}, }; use super::Response; #[post("/refresh")] pub async fn res(req: HttpRequest, data: web::Data) -> Result { - let recv_refresh_token_cookie = req.cookie("refresh_token"); + let mut refresh_token_cookie = req.cookie("refresh_token").ok_or(Error::Unauthorized("request has no refresh token".to_string()))?; - if recv_refresh_token_cookie.is_none() { - return Ok(HttpResponse::Unauthorized().finish()); - } - - let mut refresh_token = String::from(recv_refresh_token_cookie.unwrap().value()); + let mut refresh_token = String::from(refresh_token_cookie.value()); let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; @@ -47,8 +43,6 @@ pub async fn res(req: HttpRequest, data: web::Data) -> Result) -> Result Result<&str, Error> { Ok(auth_value.unwrap()) } -pub fn refresh_token_cookie(refresh_token: String) -> Cookie<'static> { +pub fn new_refresh_token_cookie(refresh_token: String) -> Cookie<'static> { Cookie::build("refresh_token", refresh_token) .http_only(true) .secure(true) From 60f0219e85a74e9a41c834179983d41689a3a9e3 Mon Sep 17 00:00:00 2001 From: Radical Date: Sat, 31 May 2025 14:43:48 +0200 Subject: [PATCH 039/160] feat: add logout endpoint --- src/api/v1/auth/logout.rs | 31 +++++++++++++++++++++++++++++++ src/api/v1/auth/mod.rs | 2 ++ 2 files changed, 33 insertions(+) create mode 100644 src/api/v1/auth/logout.rs diff --git a/src/api/v1/auth/logout.rs b/src/api/v1/auth/logout.rs new file mode 100644 index 0000000..79b5c36 --- /dev/null +++ b/src/api/v1/auth/logout.rs @@ -0,0 +1,31 @@ +use actix_web::{HttpRequest, HttpResponse, post, web}; +use diesel::{ExpressionMethods, delete}; +use diesel_async::RunQueryDsl; + +use crate::{ + Data, + error::Error, + schema::refresh_tokens::{self, dsl}, +}; + +// TODO: Should maybe be a delete request? +#[post("/logout")] +pub async fn res( + req: HttpRequest, + data: web::Data, +) -> Result { + let mut refresh_token_cookie = req.cookie("refresh_token").ok_or(Error::Unauthorized("request has no refresh token".to_string()))?; + + let refresh_token = String::from(refresh_token_cookie.value()); + + let mut conn = data.pool.get().await?; + + delete(refresh_tokens::table) + .filter(dsl::token.eq(refresh_token)) + .execute(&mut conn) + .await?; + + refresh_token_cookie.make_removal(); + + Ok(HttpResponse::Ok().cookie(refresh_token_cookie).finish()) +} diff --git a/src/api/v1/auth/mod.rs b/src/api/v1/auth/mod.rs index d627a59..75a6b0b 100644 --- a/src/api/v1/auth/mod.rs +++ b/src/api/v1/auth/mod.rs @@ -9,6 +9,7 @@ use uuid::Uuid; use crate::{Conn, error::Error, schema::access_tokens::dsl}; mod login; +mod logout; mod refresh; mod register; mod reset_password; @@ -24,6 +25,7 @@ pub fn web() -> Scope { web::scope("/auth") .service(register::res) .service(login::response) + .service(logout::res) .service(refresh::res) .service(revoke::res) .service(verify_email::get) From 1e993026a0270eb49a2c9a90ba7ade0e8895fc61 Mon Sep 17 00:00:00 2001 From: Radical Date: Sat, 31 May 2025 14:52:42 +0200 Subject: [PATCH 040/160] fix: add missing /stats to docs --- src/api/v1/stats.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/v1/stats.rs b/src/api/v1/stats.rs index 5877c00..760ec71 100644 --- a/src/api/v1/stats.rs +++ b/src/api/v1/stats.rs @@ -24,7 +24,7 @@ struct Response { build_number: String, } -/// `GET /api/v1/` Returns stats about the server +/// `GET /api/v1/stats` Returns stats about the server /// /// requires auth: no /// From 4fce2625518df3da139219cdf940a4eb449ae05e Mon Sep 17 00:00:00 2001 From: Radical Date: Sat, 31 May 2025 14:52:57 +0200 Subject: [PATCH 041/160] docs: add documentation to logout endpoint --- src/api/v1/auth/logout.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/api/v1/auth/logout.rs b/src/api/v1/auth/logout.rs index 79b5c36..d801ad7 100644 --- a/src/api/v1/auth/logout.rs +++ b/src/api/v1/auth/logout.rs @@ -8,7 +8,15 @@ use crate::{ schema::refresh_tokens::{self, dsl}, }; -// TODO: Should maybe be a delete request? +/// `GET /api/v1/logout` +/// +/// requires auth: kinda, needs refresh token set but no access token is technically required +/// +/// ### Responses +/// 200 Logged out +/// 404 Refresh token is invalid +/// 401 Unauthorized (no refresh token found) +/// #[post("/logout")] pub async fn res( req: HttpRequest, From 6783bd22a73cd86b905ac8690c83a8c4ef19a48e Mon Sep 17 00:00:00 2001 From: Radical Date: Sat, 31 May 2025 17:11:14 +0200 Subject: [PATCH 042/160] feat: add backend_url config option Required for refresh_token cookie to work properly --- src/api/v1/auth/login.rs | 2 +- src/api/v1/auth/refresh.rs | 2 +- src/api/v1/auth/register.rs | 2 +- src/config.rs | 8 ++++---- src/main.rs | 2 +- src/utils.rs | 10 ++++------ 6 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/api/v1/auth/login.rs b/src/api/v1/auth/login.rs index 04d6b4f..e190c2f 100644 --- a/src/api/v1/auth/login.rs +++ b/src/api/v1/auth/login.rs @@ -89,6 +89,6 @@ pub async fn response( .await?; Ok(HttpResponse::Ok() - .cookie(new_refresh_token_cookie(refresh_token)) + .cookie(new_refresh_token_cookie(&data.config, refresh_token)) .json(Response { access_token })) } diff --git a/src/api/v1/auth/refresh.rs b/src/api/v1/auth/refresh.rs index cc3bbe9..69bf248 100644 --- a/src/api/v1/auth/refresh.rs +++ b/src/api/v1/auth/refresh.rs @@ -85,7 +85,7 @@ pub async fn res(req: HttpRequest, data: web::Data) -> Result, port: Option, - base_path: Option, frontend_url: Url, + backend_url: Option, _ssl: Option, } @@ -86,8 +86,8 @@ impl ConfigBuilder { let web = Web { ip: self.web.ip.unwrap_or(String::from("0.0.0.0")), port: self.web.port.unwrap_or(8080), - base_path: self.web.base_path.unwrap_or("".to_string()), - frontend_url: self.web.frontend_url, + frontend_url: self.web.frontend_url.clone(), + backend_url: self.web.backend_url.or_else(|| self.web.frontend_url.join("/api").ok()).unwrap(), }; let endpoint = match &*self.bunny.endpoint { @@ -148,8 +148,8 @@ pub struct Config { pub struct Web { pub ip: String, pub port: u16, - pub base_path: String, pub frontend_url: Url, + pub backend_url: Url, } #[derive(Debug, Clone)] diff --git a/src/main.rs b/src/main.rs index 892c79d..d026f55 100644 --- a/src/main.rs +++ b/src/main.rs @@ -155,7 +155,7 @@ async fn main() -> Result<(), Error> { App::new() .app_data(web::Data::new(data.clone())) .wrap(cors) - .service(api::web(&data.config.web.base_path)) + .service(api::web(&data.config.web.backend_url.path())) }) .bind((web.ip, web.port))? .run() diff --git a/src/utils.rs b/src/utils.rs index 3241dee..f393bf5 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -16,10 +16,7 @@ use serde::Serialize; use uuid::Uuid; use crate::{ - Conn, Data, - error::Error, - schema::users, - structs::{HasIsAbove, HasUuid}, + config::Config, error::Error, schema::users, structs::{HasIsAbove, HasUuid}, Conn, Data }; pub static EMAIL_REGEX: LazyLock = LazyLock::new(|| { @@ -100,12 +97,13 @@ pub fn get_ws_protocol_header(headers: &HeaderMap) -> Result<&str, Error> { Ok(auth_value.unwrap()) } -pub fn new_refresh_token_cookie(refresh_token: String) -> Cookie<'static> { +pub fn new_refresh_token_cookie(config: &Config, refresh_token: String) -> Cookie<'static> { Cookie::build("refresh_token", refresh_token) .http_only(true) .secure(true) .same_site(SameSite::None) - .path("/api") + .domain(config.web.backend_url.domain().unwrap().to_string()) + .path(config.web.backend_url.path().to_string()) .max_age(Duration::days(30)) .finish() } From 8163d0d9c02e43d5cbb10dffd1a1a4515279cf61 Mon Sep 17 00:00:00 2001 From: Radical Date: Sat, 31 May 2025 17:51:04 +0200 Subject: [PATCH 043/160] style: clippy & fmt --- src/api/v1/auth/logout.rs | 11 +++++------ src/api/v1/auth/refresh.rs | 4 +++- src/config.rs | 6 +++++- src/utils.rs | 6 +++++- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/api/v1/auth/logout.rs b/src/api/v1/auth/logout.rs index d801ad7..0f265bb 100644 --- a/src/api/v1/auth/logout.rs +++ b/src/api/v1/auth/logout.rs @@ -8,7 +8,7 @@ use crate::{ schema::refresh_tokens::{self, dsl}, }; -/// `GET /api/v1/logout` +/// `GET /api/v1/logout` /// /// requires auth: kinda, needs refresh token set but no access token is technically required /// @@ -18,11 +18,10 @@ use crate::{ /// 401 Unauthorized (no refresh token found) /// #[post("/logout")] -pub async fn res( - req: HttpRequest, - data: web::Data, -) -> Result { - let mut refresh_token_cookie = req.cookie("refresh_token").ok_or(Error::Unauthorized("request has no refresh token".to_string()))?; +pub async fn res(req: HttpRequest, data: web::Data) -> Result { + let mut refresh_token_cookie = req.cookie("refresh_token").ok_or(Error::Unauthorized( + "request has no refresh token".to_string(), + ))?; let refresh_token = String::from(refresh_token_cookie.value()); diff --git a/src/api/v1/auth/refresh.rs b/src/api/v1/auth/refresh.rs index 69bf248..63e150e 100644 --- a/src/api/v1/auth/refresh.rs +++ b/src/api/v1/auth/refresh.rs @@ -18,7 +18,9 @@ use super::Response; #[post("/refresh")] pub async fn res(req: HttpRequest, data: web::Data) -> Result { - let mut refresh_token_cookie = req.cookie("refresh_token").ok_or(Error::Unauthorized("request has no refresh token".to_string()))?; + let mut refresh_token_cookie = req.cookie("refresh_token").ok_or(Error::Unauthorized( + "request has no refresh token".to_string(), + ))?; let mut refresh_token = String::from(refresh_token_cookie.value()); diff --git a/src/config.rs b/src/config.rs index 6b1d077..cbcc8c5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -87,7 +87,11 @@ impl ConfigBuilder { ip: self.web.ip.unwrap_or(String::from("0.0.0.0")), port: self.web.port.unwrap_or(8080), frontend_url: self.web.frontend_url.clone(), - backend_url: self.web.backend_url.or_else(|| self.web.frontend_url.join("/api").ok()).unwrap(), + backend_url: self + .web + .backend_url + .or_else(|| self.web.frontend_url.join("/api").ok()) + .unwrap(), }; let endpoint = match &*self.bunny.endpoint { diff --git a/src/utils.rs b/src/utils.rs index f393bf5..1621ddf 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -16,7 +16,11 @@ use serde::Serialize; use uuid::Uuid; use crate::{ - config::Config, error::Error, schema::users, structs::{HasIsAbove, HasUuid}, Conn, Data + Conn, Data, + config::Config, + error::Error, + schema::users, + structs::{HasIsAbove, HasUuid}, }; pub static EMAIL_REGEX: LazyLock = LazyLock::new(|| { From 042aae66f2730991d0f4719bcb877e50847ed814 Mon Sep 17 00:00:00 2001 From: Radical Date: Sat, 31 May 2025 17:52:40 +0200 Subject: [PATCH 044/160] fix: make /me/guilds return guilds instead of member objects --- src/api/v1/me/guilds.rs | 41 ++++++++++++++++++++++++++++++++--------- src/structs.rs | 34 +++++++++++++++++----------------- 2 files changed, 49 insertions(+), 26 deletions(-) diff --git a/src/api/v1/me/guilds.rs b/src/api/v1/me/guilds.rs index 06c3328..7fe02bd 100644 --- a/src/api/v1/me/guilds.rs +++ b/src/api/v1/me/guilds.rs @@ -18,16 +18,39 @@ use crate::{ /// ``` /// json!([ /// { -/// "uuid": "22006503-fb01-46e6-8e0e-70336dac6c63", -/// "nickname": "This field is nullable", -/// "user_uuid": "522bca17-de63-4706-9d18-0971867ad1e0", -/// "guild_uuid": "0911e468-3e9e-47bf-8381-59b30e8b68a8" +/// "uuid": "383d2afa-082f-4dd3-9050-ca6ed91487b6", +/// "name": "My new server!", +/// "description": null, +/// "icon": null, +/// "owner_uuid": "155d2291-fb23-46bd-a656-ae7c5d8218e6", +/// "roles": [], +/// "member_count": 1 /// }, /// { -/// "uuid": "bf95361e-3b64-4704-969c-3c5a80d10514", -/// "nickname": null, -/// "user_uuid": "522bca17-de63-4706-9d18-0971867ad1e0", -/// "guild_uuid": "69ec2ce5-3d8b-4451-b644-c2d969905458" +/// "uuid": "5ba61ec7-5f97-43e1-89a5-d4693c155612", +/// "name": "My first server!", +/// "description": "This is a cool and nullable description!", +/// "icon": "https://nullable-url/path/to/icon.png", +/// "owner_uuid": "155d2291-fb23-46bd-a656-ae7c5d8218e6", +/// "roles": [ +/// { +/// "uuid": "be0e4da4-cf73-4f45-98f8-bb1c73d1ab8b", +/// "guild_uuid": "5ba61ec7-5f97-43e1-89a5-d4693c155612", +/// "name": "Cool people", +/// "color": 15650773, +/// "is_above": c7432f1c-f4ad-4ad3-8216-51388b6abb5b, +/// "permissions": 0 +/// } +/// { +/// "uuid": "c7432f1c-f4ad-4ad3-8216-51388b6abb5b", +/// "guild_uuid": "5ba61ec7-5f97-43e1-89a5-d4693c155612", +/// "name": "Equally cool people", +/// "color": 16777215, +/// "is_above": null, +/// "permissions": 0 +/// } +/// ], +/// "member_count": 20 /// } /// ]); /// ``` @@ -46,7 +69,7 @@ pub async fn get(req: HttpRequest, data: web::Data) -> Result Result, Error> { - let mut conn = data.pool.get().await?; - + pub async fn fetch_memberships(&self, conn: &mut Conn) -> Result, Error> { use guild_members::dsl; - let member_builders: Vec = dsl::guild_members + let memberships: Vec = dsl::guild_members .filter(dsl::user_uuid.eq(self.uuid)) .select(MemberBuilder::as_select()) - .load(&mut conn) + .load(conn) .await?; - let user = User::fetch_one(data, self.uuid).await?; + let mut guilds: Vec = vec![]; - let memberships = member_builders - .iter() - .map(|m| Member { - uuid: m.uuid, - nickname: m.nickname.clone(), - user_uuid: m.user_uuid, - guild_uuid: m.guild_uuid, - user: user.clone(), - }) - .collect(); + for membership in memberships { + use guilds::dsl; + guilds.push( + dsl::guilds + .filter(dsl::uuid.eq(membership.guild_uuid)) + .select(GuildBuilder::as_select()) + .get_result(conn) + .await? + .build(conn) + .await?, + ) + } - Ok(memberships) + Ok(guilds) } pub async fn set_avatar( From 6bc2cdc3c7eac869983f6f7824a9b92327eb861e Mon Sep 17 00:00:00 2001 From: Radical Date: Sat, 31 May 2025 23:07:09 +0200 Subject: [PATCH 045/160] revert: add domain to refresh_token_cookie --- src/utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.rs b/src/utils.rs index 1621ddf..c9f3cb2 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -106,7 +106,7 @@ pub fn new_refresh_token_cookie(config: &Config, refresh_token: String) -> Cooki .http_only(true) .secure(true) .same_site(SameSite::None) - .domain(config.web.backend_url.domain().unwrap().to_string()) + //.domain(config.web.backend_url.domain().unwrap().to_string()) .path(config.web.backend_url.path().to_string()) .max_age(Duration::days(30)) .finish() From cade49d9c63f62ce7146390860170e29a197b455 Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 1 Jun 2025 01:47:30 +0200 Subject: [PATCH 046/160] fix: return empty vector instead of 404 error --- src/structs.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/structs.rs b/src/structs.rs index 38f2982..0f7afdc 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -933,11 +933,13 @@ impl Me { pub async fn fetch_memberships(&self, conn: &mut Conn) -> Result, Error> { use guild_members::dsl; - let memberships: Vec = dsl::guild_members - .filter(dsl::user_uuid.eq(self.uuid)) - .select(MemberBuilder::as_select()) - .load(conn) - .await?; + let memberships: Vec = load_or_empty( + dsl::guild_members + .filter(dsl::user_uuid.eq(self.uuid)) + .select(MemberBuilder::as_select()) + .load(conn) + .await + )?; let mut guilds: Vec = vec![]; From 57f52d96df40e9e878dd8112c678f5a920817117 Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 1 Jun 2025 14:09:38 +0200 Subject: [PATCH 047/160] feat: expire cache when updating user --- src/api/v1/me/mod.rs | 9 ++++----- src/structs.rs | 46 +++++++++++++++++++++++++++++++++----------- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/src/api/v1/me/mod.rs b/src/api/v1/me/mod.rs index 00a1ec3..4a4c1e8 100644 --- a/src/api/v1/me/mod.rs +++ b/src/api/v1/me/mod.rs @@ -81,8 +81,7 @@ pub async fn update( let byte_slice: &[u8] = &bytes; me.set_avatar( - &data.bunny_cdn, - &mut conn, + &data, data.config.bunny.cdn_url.clone(), byte_slice.into(), ) @@ -91,15 +90,15 @@ pub async fn update( if let Some(new_info) = form.json.0 { if let Some(username) = &new_info.username { - me.set_username(&mut conn, username.clone()).await?; + me.set_username(&data, username.clone()).await?; } if let Some(display_name) = &new_info.display_name { - me.set_display_name(&mut conn, display_name.clone()).await?; + me.set_display_name(&data, display_name.clone()).await?; } if let Some(email) = &new_info.email { - me.set_email(&mut conn, email.to_string()).await?; + me.set_email(&data, email.to_string()).await?; } } diff --git a/src/structs.rs b/src/structs.rs index 0f7afdc..0dada1a 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -961,25 +961,27 @@ impl Me { pub async fn set_avatar( &mut self, - bunny_cdn: &bunny_api_tokio::Client, - conn: &mut Conn, + data: &Data, cdn_url: Url, avatar: BytesMut, ) -> Result<(), Error> { let avatar_clone = avatar.clone(); let image_type = task::spawn_blocking(move || image_check(avatar_clone)).await??; + let mut conn = data.pool.get().await?; + if let Some(avatar) = &self.avatar { let avatar_url: Url = avatar.parse()?; let relative_url = avatar_url.path().trim_start_matches('/'); - bunny_cdn.storage.delete(relative_url).await?; + data.bunny_cdn.storage.delete(relative_url).await?; } let path = format!("avatar/{}/avatar.{}", self.uuid, image_type); - bunny_cdn + data. + bunny_cdn .storage .upload(path.clone(), avatar.into()) .await?; @@ -990,9 +992,13 @@ impl Me { update(users::table) .filter(dsl::uuid.eq(self.uuid)) .set(dsl::avatar.eq(avatar_url.as_str())) - .execute(conn) + .execute(&mut conn) .await?; + if data.get_cache_key(self.uuid.to_string()).await.is_ok() { + data.del_cache_key(self.uuid.to_string()).await? + } + self.avatar = Some(avatar_url.to_string()); Ok(()) @@ -1011,20 +1017,26 @@ impl Me { pub async fn set_username( &mut self, - conn: &mut Conn, + data: &Data, new_username: String, ) -> Result<(), Error> { if !USERNAME_REGEX.is_match(&new_username) { return Err(Error::BadRequest("Invalid username".to_string())); } + let mut conn = data.pool.get().await?; + use users::dsl; update(users::table) .filter(dsl::uuid.eq(self.uuid)) .set(dsl::username.eq(new_username.as_str())) - .execute(conn) + .execute(&mut conn) .await?; + if data.get_cache_key(self.uuid.to_string()).await.is_ok() { + data.del_cache_key(self.uuid.to_string()).await? + } + self.username = new_username; Ok(()) @@ -1032,26 +1044,34 @@ impl Me { pub async fn set_display_name( &mut self, - conn: &mut Conn, + data: &Data, new_display_name: String, ) -> Result<(), Error> { + let mut conn = data.pool.get().await?; + use users::dsl; update(users::table) .filter(dsl::uuid.eq(self.uuid)) .set(dsl::display_name.eq(new_display_name.as_str())) - .execute(conn) + .execute(&mut conn) .await?; + if data.get_cache_key(self.uuid.to_string()).await.is_ok() { + data.del_cache_key(self.uuid.to_string()).await? + } + self.display_name = Some(new_display_name); Ok(()) } - pub async fn set_email(&mut self, conn: &mut Conn, new_email: String) -> Result<(), Error> { + pub async fn set_email(&mut self, data: &Data, new_email: String) -> Result<(), Error> { if !EMAIL_REGEX.is_match(&new_email) { return Err(Error::BadRequest("Invalid username".to_string())); } + let mut conn = data.pool.get().await?; + use users::dsl; update(users::table) .filter(dsl::uuid.eq(self.uuid)) @@ -1059,9 +1079,13 @@ impl Me { dsl::email.eq(new_email.as_str()), dsl::email_verified.eq(false), )) - .execute(conn) + .execute(&mut conn) .await?; + if data.get_cache_key(self.uuid.to_string()).await.is_ok() { + data.del_cache_key(self.uuid.to_string()).await? + } + self.email = new_email; Ok(()) From 2f7fac8db5cbc87e4d2d10c0ba0266d2e0c9d27e Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 1 Jun 2025 14:22:52 +0200 Subject: [PATCH 048/160] fix: dont use option in MpJson --- src/api/v1/me/mod.rs | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/api/v1/me/mod.rs b/src/api/v1/me/mod.rs index 4a4c1e8..e7c8167 100644 --- a/src/api/v1/me/mod.rs +++ b/src/api/v1/me/mod.rs @@ -46,7 +46,7 @@ struct NewInfo { struct UploadForm { #[multipart(limit = "100MB")] avatar: Option, - json: MpJson>, + json: MpJson, } #[patch("")] @@ -64,11 +64,8 @@ pub async fn update( let uuid = check_access_token(auth_header, &mut conn).await?; if form.avatar.is_some() - || form - .json - .0 - .clone() - .is_some_and(|ni| ni.username.is_some() || ni.display_name.is_some()) + || form.json.username.is_some() + || form.json.display_name.is_some() { global_checks(&data, uuid).await?; } @@ -88,18 +85,16 @@ pub async fn update( .await?; } - if let Some(new_info) = form.json.0 { - if let Some(username) = &new_info.username { - me.set_username(&data, username.clone()).await?; - } + if let Some(username) = &form.json.username { + me.set_username(&data, username.clone()).await?; + } - if let Some(display_name) = &new_info.display_name { - me.set_display_name(&data, display_name.clone()).await?; - } + if let Some(display_name) = &form.json.display_name { + me.set_display_name(&data, display_name.clone()).await?; + } - if let Some(email) = &new_info.email { - me.set_email(&data, email.to_string()).await?; - } + if let Some(email) = &form.json.email { + me.set_email(&data, email.clone()).await?; } Ok(HttpResponse::Ok().finish()) From ee8211a3216a5f0f2aeb8f2154eec78ba9027aa3 Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 1 Jun 2025 15:58:07 +0200 Subject: [PATCH 049/160] feat: add pronouns to users --- .../down.sql | 2 ++ .../up.sql | 2 ++ src/api/v1/me/mod.rs | 5 +++++ src/schema.rs | 2 ++ src/structs.rs | 21 +++++++++++++++++++ 5 files changed, 32 insertions(+) create mode 100644 migrations/2025-06-01-134036_add_pronouns_to_users/down.sql create mode 100644 migrations/2025-06-01-134036_add_pronouns_to_users/up.sql diff --git a/migrations/2025-06-01-134036_add_pronouns_to_users/down.sql b/migrations/2025-06-01-134036_add_pronouns_to_users/down.sql new file mode 100644 index 0000000..32d891f --- /dev/null +++ b/migrations/2025-06-01-134036_add_pronouns_to_users/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE users DROP COLUMN pronouns; diff --git a/migrations/2025-06-01-134036_add_pronouns_to_users/up.sql b/migrations/2025-06-01-134036_add_pronouns_to_users/up.sql new file mode 100644 index 0000000..90807bb --- /dev/null +++ b/migrations/2025-06-01-134036_add_pronouns_to_users/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE users ADD COLUMN pronouns VARCHAR(32) DEFAULT NULL; diff --git a/src/api/v1/me/mod.rs b/src/api/v1/me/mod.rs index e7c8167..fc9e61b 100644 --- a/src/api/v1/me/mod.rs +++ b/src/api/v1/me/mod.rs @@ -40,6 +40,7 @@ struct NewInfo { display_name: Option, //password: Option, will probably be handled through a reset password link email: Option, + pronouns: Option, } #[derive(Debug, MultipartForm)] @@ -97,5 +98,9 @@ pub async fn update( me.set_email(&data, email.clone()).await?; } + if let Some(pronouns) = &form.json.pronouns { + me.set_pronouns(&data, pronouns.clone()).await?; + } + Ok(HttpResponse::Ok().finish()) } diff --git a/src/schema.rs b/src/schema.rs index 1b34400..3be885a 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -144,6 +144,8 @@ diesel::table! { deleted_at -> Nullable, #[max_length = 100] avatar -> Nullable, + #[max_length = 32] + pronouns -> Nullable, } } diff --git a/src/structs.rs b/src/structs.rs index 0dada1a..5e19dad 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -865,6 +865,7 @@ pub struct User { username: String, display_name: Option, avatar: Option, + pronouns: Option, } impl User { @@ -915,6 +916,7 @@ pub struct Me { username: String, display_name: Option, avatar: Option, + pronouns: Option, email: String, pub email_verified: bool, } @@ -1090,6 +1092,25 @@ impl Me { Ok(()) } + + pub async fn set_pronouns(&mut self, data: &Data, new_pronouns: String) -> Result<(), Error> { + let mut conn = data.pool.get().await?; + + use users::dsl; + update(users::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(( + dsl::pronouns.eq(new_pronouns.as_str()), + )) + .execute(&mut conn) + .await?; + + if data.get_cache_key(self.uuid.to_string()).await.is_ok() { + data.del_cache_key(self.uuid.to_string()).await? + } + + Ok(()) + } } #[derive(Deserialize)] From 643f94b5805f616dcf79cb663f76236c99ef685a Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 1 Jun 2025 21:56:47 +0200 Subject: [PATCH 050/160] ci: add proper cross compiling! --- .woodpecker/build-and-publish.yml | 15 +++++++++++++++ Dockerfile | 11 ++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/.woodpecker/build-and-publish.yml b/.woodpecker/build-and-publish.yml index c0f367a..57f2761 100644 --- a/.woodpecker/build-and-publish.yml +++ b/.woodpecker/build-and-publish.yml @@ -3,6 +3,21 @@ when: branch: main steps: + - name: build-x86_64 + image: rust:bookworm + commands: + - cargo build --release + - name: build-arm64 + image: rust:bookworm + commands: + - dpkg --add-architecture arm64 + - apt-get update -y && apt-get install -y crossbuild-essential-arm64 libssl-dev:arm64 + - rustup target add aarch64-unknown-linux-gnu + - cargo build --target aarch64-unknown-linux-gnu --release + environment: + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc + PKG_CONFIG_ALLOW_CROSS: 1 + PKG_CONFIG_PATH: /usr/aarch64-linux-gnu/lib/pkgconfig - name: container-build-and-publish image: docker commands: diff --git a/Dockerfile b/Dockerfile index d7209ef..25795a9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,17 @@ -FROM rust:bookworm AS builder +FROM --platform=linux/amd64 debian:12-slim AS prep WORKDIR /src -COPY . . - -RUN cargo build --release +COPY target/release/backend backend-amd64 +COPY target/aarch64-unknown-linux-gnu/release/backend backend-arm64 FROM debian:12-slim +ARG TARGETARCH + RUN apt update -y && apt install libssl3 ca-certificates -y && rm -rf /var/lib/apt/lists/* /var/cache/apt/* /tmp/* -COPY --from=builder /src/target/release/backend /usr/bin/gorb-backend +COPY --from=prep /src/backend-${TARGETARCH} /usr/bin/gorb-backend COPY entrypoint.sh /usr/bin/entrypoint.sh From 15eb1027845e1a322f9f6d08387389226d6a3c3c Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 1 Jun 2025 22:10:23 +0200 Subject: [PATCH 051/160] build: try to make dev bearable --- Cargo.toml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 492a284..30b5827 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,12 @@ strip = true lto = true codegen-units = 1 +# Speed up compilation to make dev bearable +[profile.dev] +debug = 0 +strip = "debuginfo" +codegen-units = 512 + [dependencies] actix-cors = "0.7.1" actix-web = "4.11" @@ -32,7 +38,7 @@ futures-util = "0.3.31" bunny-api-tokio = "0.3.0" bindet = "0.3.2" deadpool = "0.12" -diesel = { version = "2.2", features = ["uuid", "chrono"] } +diesel = { version = "2.2", features = ["uuid", "chrono"], default-features = false } diesel-async = { version = "0.5", features = ["deadpool", "postgres", "async-connection-wrapper"] } diesel_migrations = { version = "2.2.0", features = ["postgres"] } thiserror = "2.0.12" From 41defc4a252bf577b96d4d125f7a4335dc9d4b17 Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 1 Jun 2025 22:10:37 +0200 Subject: [PATCH 052/160] feat: add patch request to channels! --- src/api/v1/channels/uuid/mod.rs | 82 +++++++++++++++++++++++++- src/structs.rs | 101 +++++++++++++++++++++++++++++--- src/utils.rs | 3 + 3 files changed, 178 insertions(+), 8 deletions(-) diff --git a/src/api/v1/channels/uuid/mod.rs b/src/api/v1/channels/uuid/mod.rs index f429159..1874dfc 100644 --- a/src/api/v1/channels/uuid/mod.rs +++ b/src/api/v1/channels/uuid/mod.rs @@ -1,3 +1,5 @@ +//! `/api/v1/channels/{uuid}` Channel specific endpoints + pub mod messages; pub mod socket; @@ -8,8 +10,9 @@ use crate::{ structs::{Channel, Member}, utils::{get_auth_header, global_checks}, }; -use actix_web::{HttpRequest, HttpResponse, delete, get, web}; +use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse}; use uuid::Uuid; +use serde::Deserialize; #[get("/{uuid}")] pub async fn get( @@ -62,3 +65,80 @@ pub async fn delete( Ok(HttpResponse::Ok().finish()) } + +#[derive(Deserialize)] +struct NewInfo { + name: Option, + description: Option, + is_above: Option, +} + +/// `PATCH /api/v1/channels/{uuid}` Returns user with the given UUID +/// +/// requires auth: yes +/// +/// requires relation: yes +/// +/// ### Request Example +/// All fields are optional and can be nulled/dropped if only changing 1 value +/// ``` +/// json!({ +/// "name": "gaming-chat", +/// "description": "Gaming related topics.", +/// "is_above": "398f6d7b-752c-4348-9771-fe6024adbfb1" +/// }); +/// ``` +/// +/// ### Response Example +/// ``` +/// json!({ +/// uuid: "cdcac171-5add-4f88-9559-3a247c8bba2c", +/// guild_uuid: "383d2afa-082f-4dd3-9050-ca6ed91487b6", +/// name: "gaming-chat", +/// description: "Gaming related topics.", +/// is_above: "398f6d7b-752c-4348-9771-fe6024adbfb1", +/// permissions: { +/// role_uuid: "79cc0806-0f37-4a06-a468-6639c4311a2d", +/// permissions: 0 +/// } +/// }); +/// ``` +/// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps +#[patch("/{uuid}")] +pub async fn patch( + req: HttpRequest, + path: web::Path<(Uuid,)>, + new_info: web::Json, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let channel_uuid = path.into_inner().0; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + global_checks(&data, uuid).await?; + + let mut channel = Channel::fetch_one(&data, channel_uuid).await?; + + Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; + + if let Some(new_name) = &new_info.name { + channel.set_name(&data, new_name.to_string()).await?; + } + + if let Some(new_description) = &new_info.description { + channel.set_description(&data, new_description.to_string()).await?; + } + + if let Some(new_is_above) = &new_info.is_above { + channel.set_description(&data, new_is_above.to_string()).await?; + } + + Ok(HttpResponse::Ok().json(channel)) +} + diff --git a/src/structs.rs b/src/structs.rs index 5e19dad..b955133 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -24,13 +24,9 @@ use url::Url; use uuid::Uuid; use crate::{ - Conn, Data, - error::Error, - schema::*, - utils::{ - EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX, generate_refresh_token, global_checks, - image_check, order_by_is_above, user_uuid_from_identifier, - }, + error::Error, schema::*, utils::{ + generate_refresh_token, global_checks, image_check, order_by_is_above, user_uuid_from_identifier, CHANNEL_REGEX, EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX + }, Conn, Data }; pub trait HasUuid { @@ -231,6 +227,10 @@ impl Channel { name: String, description: Option, ) -> Result { + if !CHANNEL_REGEX.is_match(&name) { + return Err(Error::BadRequest("Channel name is invalid".to_string())) + } + let mut conn = data.pool.get().await?; let channel_uuid = Uuid::now_v7(); @@ -353,6 +353,93 @@ impl Channel { message.build(data).await } + + pub async fn set_name(&mut self, data: &Data, new_name: String) -> Result<(), Error> { + if !CHANNEL_REGEX.is_match(&new_name) { + return Err(Error::BadRequest("Channel name is invalid".to_string())) + } + + let mut conn = data.pool.get().await?; + + use channels::dsl; + update(channels::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(dsl::name.eq(&new_name)) + .execute(&mut conn) + .await?; + + self.name = new_name; + + Ok(()) + } + + pub async fn set_description(&mut self, data: &Data, new_description: String) -> Result<(), Error> { + let mut conn = data.pool.get().await?; + + use channels::dsl; + update(channels::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(dsl::description.eq(&new_description)) + .execute(&mut conn) + .await?; + + self.description = Some(new_description); + + Ok(()) + } + + pub async fn move_channel(&mut self, data: &Data, new_is_above: Uuid) -> Result<(), Error> { + let mut conn = data.pool.get().await?; + + use channels::dsl; + let old_above_uuid: Option = match dsl::channels + .filter(dsl::is_above.eq(self.uuid)) + .select(dsl::uuid) + .get_result(&mut conn) + .await + { + Ok(r) => Ok(Some(r)), + Err(e) if e == diesel::result::Error::NotFound => Ok(None), + Err(e) => Err(e), + }?; + + if let Some(uuid) = old_above_uuid { + update(channels::table) + .filter(dsl::uuid.eq(uuid)) + .set(dsl::is_above.eq(None::)) + .execute(&mut conn) + .await?; + } + + match update(channels::table) + .filter(dsl::is_above.eq(new_is_above)) + .set(dsl::is_above.eq(self.uuid)) + .execute(&mut conn) + .await + { + Ok(r) => Ok(r), + Err(e) if e == diesel::result::Error::NotFound => Ok(0), + Err(e) => Err(e), + }?; + + update(channels::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(dsl::is_above.eq(new_is_above)) + .execute(&mut conn) + .await?; + + if let Some(uuid) = old_above_uuid { + update(channels::table) + .filter(dsl::uuid.eq(uuid)) + .set(dsl::is_above.eq(self.is_above)) + .execute(&mut conn) + .await?; + } + + self.is_above = Some(new_is_above); + + Ok(()) + } } #[derive(Clone, Copy)] diff --git a/src/utils.rs b/src/utils.rs index c9f3cb2..3bf7332 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -30,6 +30,9 @@ pub static EMAIL_REGEX: LazyLock = LazyLock::new(|| { pub static USERNAME_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"^[a-z0-9_.-]+$").unwrap()); +pub static CHANNEL_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r"^[a-z0-9_.-]+$").unwrap()); + // Password is expected to be hashed using SHA3-384 pub static PASSWORD_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"[0-9a-f]{96}").unwrap()); From c4fc23ec85f5da1422688279de9848b1e710abf8 Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 1 Jun 2025 22:20:29 +0200 Subject: [PATCH 053/160] feat: add about to users --- .../down.sql | 2 ++ .../up.sql | 2 ++ src/api/v1/me/mod.rs | 5 +++++ src/schema.rs | 2 ++ src/structs.rs | 21 +++++++++++++++++++ 5 files changed, 32 insertions(+) create mode 100644 migrations/2025-06-01-143713_add_about_to_users/down.sql create mode 100644 migrations/2025-06-01-143713_add_about_to_users/up.sql diff --git a/migrations/2025-06-01-143713_add_about_to_users/down.sql b/migrations/2025-06-01-143713_add_about_to_users/down.sql new file mode 100644 index 0000000..de48d07 --- /dev/null +++ b/migrations/2025-06-01-143713_add_about_to_users/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE users DROP COLUMN about; \ No newline at end of file diff --git a/migrations/2025-06-01-143713_add_about_to_users/up.sql b/migrations/2025-06-01-143713_add_about_to_users/up.sql new file mode 100644 index 0000000..54b5449 --- /dev/null +++ b/migrations/2025-06-01-143713_add_about_to_users/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE users ADD COLUMN about VARCHAR(200) DEFAULT NULL; diff --git a/src/api/v1/me/mod.rs b/src/api/v1/me/mod.rs index fc9e61b..ac35140 100644 --- a/src/api/v1/me/mod.rs +++ b/src/api/v1/me/mod.rs @@ -41,6 +41,7 @@ struct NewInfo { //password: Option, will probably be handled through a reset password link email: Option, pronouns: Option, + about: Option, } #[derive(Debug, MultipartForm)] @@ -102,5 +103,9 @@ pub async fn update( me.set_pronouns(&data, pronouns.clone()).await?; } + if let Some(about) = &form.json.about { + me.set_about(&data, about.clone()).await?; + } + Ok(HttpResponse::Ok().finish()) } diff --git a/src/schema.rs b/src/schema.rs index 3be885a..09ea7a3 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -146,6 +146,8 @@ diesel::table! { avatar -> Nullable, #[max_length = 32] pronouns -> Nullable, + #[max_length = 200] + about -> Nullable, } } diff --git a/src/structs.rs b/src/structs.rs index b955133..b68aaf5 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -953,6 +953,7 @@ pub struct User { display_name: Option, avatar: Option, pronouns: Option, + about: Option, } impl User { @@ -1004,6 +1005,7 @@ pub struct Me { display_name: Option, avatar: Option, pronouns: Option, + about: Option, email: String, pub email_verified: bool, } @@ -1198,6 +1200,25 @@ impl Me { Ok(()) } + + pub async fn set_about(&mut self, data: &Data, new_about: String) -> Result<(), Error> { + let mut conn = data.pool.get().await?; + + use users::dsl; + update(users::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(( + dsl::about.eq(new_about.as_str()), + )) + .execute(&mut conn) + .await?; + + if data.get_cache_key(self.uuid.to_string()).await.is_ok() { + data.del_cache_key(self.uuid.to_string()).await? + } + + Ok(()) + } } #[derive(Deserialize)] From 08cb70ce18b27966b118b11a02bda654d2c79947 Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 1 Jun 2025 23:43:14 +0200 Subject: [PATCH 054/160] fix: add patch request as a service in actix whoops forgot to add /channels/{uuid} patch request into actix --- src/api/v1/channels/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/api/v1/channels/mod.rs b/src/api/v1/channels/mod.rs index 999bb23..e9558c9 100644 --- a/src/api/v1/channels/mod.rs +++ b/src/api/v1/channels/mod.rs @@ -6,6 +6,7 @@ pub fn web() -> Scope { web::scope("/channels") .service(uuid::get) .service(uuid::delete) + .service(uuid::patch) .service(uuid::messages::get) .service(uuid::socket::ws) } From 7021c80f0200931c247192953bb0f265688bf1d3 Mon Sep 17 00:00:00 2001 From: Radical Date: Mon, 2 Jun 2025 00:28:48 +0200 Subject: [PATCH 055/160] style: move structs to objects and split into several files for readability --- src/api/v1/auth/reset_password.rs | 2 +- src/api/v1/auth/verify_email.rs | 2 +- src/api/v1/channels/uuid/messages.rs | 2 +- src/api/v1/channels/uuid/mod.rs | 17 +- src/api/v1/channels/uuid/socket.rs | 2 +- src/api/v1/guilds/mod.rs | 2 +- src/api/v1/guilds/uuid/channels.rs | 2 +- src/api/v1/guilds/uuid/icon.rs | 2 +- src/api/v1/guilds/uuid/invites/mod.rs | 2 +- src/api/v1/guilds/uuid/members.rs | 2 +- src/api/v1/guilds/uuid/mod.rs | 2 +- src/api/v1/guilds/uuid/roles/mod.rs | 2 +- src/api/v1/guilds/uuid/roles/uuid.rs | 2 +- src/api/v1/invites/id.rs | 2 +- src/api/v1/me/guilds.rs | 2 +- src/api/v1/me/mod.rs | 15 +- src/api/v1/users/mod.rs | 2 +- src/api/v1/users/uuid.rs | 2 +- src/main.rs | 4 +- src/objects/channel.rs | 353 ++++++ src/objects/email_token.rs | 80 ++ src/objects/guild.rs | 226 ++++ src/objects/invite.rs | 30 + src/objects/me.rs | 232 ++++ src/objects/member.rs | 125 +++ src/objects/message.rs | 40 + src/objects/mod.rs | 156 +++ src/objects/password_reset_token.rs | 160 +++ src/objects/role.rs | 96 ++ src/objects/user.rs | 60 ++ src/structs.rs | 1437 ------------------------- src/utils.rs | 2 +- 32 files changed, 1591 insertions(+), 1474 deletions(-) create mode 100644 src/objects/channel.rs create mode 100644 src/objects/email_token.rs create mode 100644 src/objects/guild.rs create mode 100644 src/objects/invite.rs create mode 100644 src/objects/me.rs create mode 100644 src/objects/member.rs create mode 100644 src/objects/message.rs create mode 100644 src/objects/mod.rs create mode 100644 src/objects/password_reset_token.rs create mode 100644 src/objects/role.rs create mode 100644 src/objects/user.rs delete mode 100644 src/structs.rs diff --git a/src/api/v1/auth/reset_password.rs b/src/api/v1/auth/reset_password.rs index 8240fbd..4373a82 100644 --- a/src/api/v1/auth/reset_password.rs +++ b/src/api/v1/auth/reset_password.rs @@ -4,7 +4,7 @@ use actix_web::{HttpResponse, get, post, web}; use chrono::{Duration, Utc}; use serde::Deserialize; -use crate::{Data, error::Error, structs::PasswordResetToken}; +use crate::{Data, error::Error, objects::PasswordResetToken}; #[derive(Deserialize)] struct Query { diff --git a/src/api/v1/auth/verify_email.rs b/src/api/v1/auth/verify_email.rs index c5c9097..0f23649 100644 --- a/src/api/v1/auth/verify_email.rs +++ b/src/api/v1/auth/verify_email.rs @@ -8,7 +8,7 @@ use crate::{ Data, api::v1::auth::check_access_token, error::Error, - structs::{EmailToken, Me}, + objects::{EmailToken, Me}, utils::get_auth_header, }; diff --git a/src/api/v1/channels/uuid/messages.rs b/src/api/v1/channels/uuid/messages.rs index ddcc800..9fdea0b 100644 --- a/src/api/v1/channels/uuid/messages.rs +++ b/src/api/v1/channels/uuid/messages.rs @@ -4,7 +4,7 @@ use crate::{ Data, api::v1::auth::check_access_token, error::Error, - structs::{Channel, Member}, + objects::{Channel, Member}, utils::{get_auth_header, global_checks}, }; use ::uuid::Uuid; diff --git a/src/api/v1/channels/uuid/mod.rs b/src/api/v1/channels/uuid/mod.rs index 1874dfc..1cb20c7 100644 --- a/src/api/v1/channels/uuid/mod.rs +++ b/src/api/v1/channels/uuid/mod.rs @@ -7,12 +7,12 @@ use crate::{ Data, api::v1::auth::check_access_token, error::Error, - structs::{Channel, Member}, + objects::{Channel, Member}, utils::{get_auth_header, global_checks}, }; -use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse}; -use uuid::Uuid; +use actix_web::{HttpRequest, HttpResponse, delete, get, patch, web}; use serde::Deserialize; +use uuid::Uuid; #[get("/{uuid}")] pub async fn get( @@ -88,7 +88,7 @@ struct NewInfo { /// "is_above": "398f6d7b-752c-4348-9771-fe6024adbfb1" /// }); /// ``` -/// +/// /// ### Response Example /// ``` /// json!({ @@ -132,13 +132,16 @@ pub async fn patch( } if let Some(new_description) = &new_info.description { - channel.set_description(&data, new_description.to_string()).await?; + channel + .set_description(&data, new_description.to_string()) + .await?; } if let Some(new_is_above) = &new_info.is_above { - channel.set_description(&data, new_is_above.to_string()).await?; + channel + .set_description(&data, new_is_above.to_string()) + .await?; } Ok(HttpResponse::Ok().json(channel)) } - diff --git a/src/api/v1/channels/uuid/socket.rs b/src/api/v1/channels/uuid/socket.rs index 556dca3..b346e8e 100644 --- a/src/api/v1/channels/uuid/socket.rs +++ b/src/api/v1/channels/uuid/socket.rs @@ -10,7 +10,7 @@ use uuid::Uuid; use crate::{ Data, api::v1::auth::check_access_token, - structs::{Channel, Member}, + objects::{Channel, Member}, utils::{get_ws_protocol_header, global_checks}, }; diff --git a/src/api/v1/guilds/mod.rs b/src/api/v1/guilds/mod.rs index b7a7a7c..ada5dc8 100644 --- a/src/api/v1/guilds/mod.rs +++ b/src/api/v1/guilds/mod.rs @@ -9,7 +9,7 @@ use crate::{ Data, api::v1::auth::check_access_token, error::Error, - structs::{Guild, StartAmountQuery}, + objects::{Guild, StartAmountQuery}, utils::{get_auth_header, global_checks}, }; diff --git a/src/api/v1/guilds/uuid/channels.rs b/src/api/v1/guilds/uuid/channels.rs index 0dd4566..083553a 100644 --- a/src/api/v1/guilds/uuid/channels.rs +++ b/src/api/v1/guilds/uuid/channels.rs @@ -2,7 +2,7 @@ use crate::{ Data, api::v1::auth::check_access_token, error::Error, - structs::{Channel, Member}, + objects::{Channel, Member}, utils::{get_auth_header, global_checks, order_by_is_above}, }; use ::uuid::Uuid; diff --git a/src/api/v1/guilds/uuid/icon.rs b/src/api/v1/guilds/uuid/icon.rs index f2e15b6..5025416 100644 --- a/src/api/v1/guilds/uuid/icon.rs +++ b/src/api/v1/guilds/uuid/icon.rs @@ -8,7 +8,7 @@ use crate::{ Data, api::v1::auth::check_access_token, error::Error, - structs::{Guild, Member}, + objects::{Guild, Member}, utils::{get_auth_header, global_checks}, }; diff --git a/src/api/v1/guilds/uuid/invites/mod.rs b/src/api/v1/guilds/uuid/invites/mod.rs index ea04529..f4f06bc 100644 --- a/src/api/v1/guilds/uuid/invites/mod.rs +++ b/src/api/v1/guilds/uuid/invites/mod.rs @@ -6,7 +6,7 @@ use crate::{ Data, api::v1::auth::check_access_token, error::Error, - structs::{Guild, Member}, + objects::{Guild, Member}, utils::{get_auth_header, global_checks}, }; diff --git a/src/api/v1/guilds/uuid/members.rs b/src/api/v1/guilds/uuid/members.rs index d7ed0a5..972d862 100644 --- a/src/api/v1/guilds/uuid/members.rs +++ b/src/api/v1/guilds/uuid/members.rs @@ -2,7 +2,7 @@ use crate::{ Data, api::v1::auth::check_access_token, error::Error, - structs::Member, + objects::Member, utils::{get_auth_header, global_checks}, }; use ::uuid::Uuid; diff --git a/src/api/v1/guilds/uuid/mod.rs b/src/api/v1/guilds/uuid/mod.rs index c24e957..4c88d7a 100644 --- a/src/api/v1/guilds/uuid/mod.rs +++ b/src/api/v1/guilds/uuid/mod.rs @@ -13,7 +13,7 @@ use crate::{ Data, api::v1::auth::check_access_token, error::Error, - structs::{Guild, Member}, + objects::{Guild, Member}, utils::{get_auth_header, global_checks}, }; diff --git a/src/api/v1/guilds/uuid/roles/mod.rs b/src/api/v1/guilds/uuid/roles/mod.rs index 8015384..717b30b 100644 --- a/src/api/v1/guilds/uuid/roles/mod.rs +++ b/src/api/v1/guilds/uuid/roles/mod.rs @@ -6,7 +6,7 @@ use crate::{ Data, api::v1::auth::check_access_token, error::Error, - structs::{Member, Role}, + objects::{Member, Role}, utils::{get_auth_header, global_checks, order_by_is_above}, }; diff --git a/src/api/v1/guilds/uuid/roles/uuid.rs b/src/api/v1/guilds/uuid/roles/uuid.rs index 0e7f306..f1a3206 100644 --- a/src/api/v1/guilds/uuid/roles/uuid.rs +++ b/src/api/v1/guilds/uuid/roles/uuid.rs @@ -2,7 +2,7 @@ use crate::{ Data, api::v1::auth::check_access_token, error::Error, - structs::{Member, Role}, + objects::{Member, Role}, utils::{get_auth_header, global_checks}, }; use ::uuid::Uuid; diff --git a/src/api/v1/invites/id.rs b/src/api/v1/invites/id.rs index 687b825..22e2868 100644 --- a/src/api/v1/invites/id.rs +++ b/src/api/v1/invites/id.rs @@ -4,7 +4,7 @@ use crate::{ Data, api::v1::auth::check_access_token, error::Error, - structs::{Guild, Invite, Member}, + objects::{Guild, Invite, Member}, utils::{get_auth_header, global_checks}, }; diff --git a/src/api/v1/me/guilds.rs b/src/api/v1/me/guilds.rs index 7fe02bd..71cfca4 100644 --- a/src/api/v1/me/guilds.rs +++ b/src/api/v1/me/guilds.rs @@ -6,7 +6,7 @@ use crate::{ Data, api::v1::auth::check_access_token, error::Error, - structs::Me, + objects::Me, utils::{get_auth_header, global_checks}, }; diff --git a/src/api/v1/me/mod.rs b/src/api/v1/me/mod.rs index ac35140..da5c929 100644 --- a/src/api/v1/me/mod.rs +++ b/src/api/v1/me/mod.rs @@ -6,7 +6,7 @@ use crate::{ Data, api::v1::auth::check_access_token, error::Error, - structs::Me, + objects::Me, utils::{get_auth_header, global_checks}, }; @@ -65,10 +65,7 @@ pub async fn update( let uuid = check_access_token(auth_header, &mut conn).await?; - if form.avatar.is_some() - || form.json.username.is_some() - || form.json.display_name.is_some() - { + if form.avatar.is_some() || form.json.username.is_some() || form.json.display_name.is_some() { global_checks(&data, uuid).await?; } @@ -79,12 +76,8 @@ pub async fn update( let byte_slice: &[u8] = &bytes; - me.set_avatar( - &data, - data.config.bunny.cdn_url.clone(), - byte_slice.into(), - ) - .await?; + me.set_avatar(&data, data.config.bunny.cdn_url.clone(), byte_slice.into()) + .await?; } if let Some(username) = &form.json.username { diff --git a/src/api/v1/users/mod.rs b/src/api/v1/users/mod.rs index fd3980d..334fd5f 100644 --- a/src/api/v1/users/mod.rs +++ b/src/api/v1/users/mod.rs @@ -6,7 +6,7 @@ use crate::{ Data, api::v1::auth::check_access_token, error::Error, - structs::{StartAmountQuery, User}, + objects::{StartAmountQuery, User}, utils::{get_auth_header, global_checks}, }; diff --git a/src/api/v1/users/uuid.rs b/src/api/v1/users/uuid.rs index 213afe5..9e602a0 100644 --- a/src/api/v1/users/uuid.rs +++ b/src/api/v1/users/uuid.rs @@ -7,7 +7,7 @@ use crate::{ Data, api::v1::auth::check_access_token, error::Error, - structs::User, + objects::User, utils::{get_auth_header, global_checks}, }; diff --git a/src/main.rs b/src/main.rs index d026f55..9a24d3d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,9 +5,9 @@ use clap::Parser; use diesel_async::pooled_connection::AsyncDieselConnectionManager; use diesel_async::pooled_connection::deadpool::Pool; use error::Error; +use objects::MailClient; use simple_logger::SimpleLogger; use std::time::SystemTime; -use structs::MailClient; mod config; use config::{Config, ConfigBuilder}; use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations}; @@ -19,8 +19,8 @@ type Conn = mod api; pub mod error; +pub mod objects; pub mod schema; -pub mod structs; pub mod utils; #[derive(Parser, Debug)] diff --git a/src/objects/channel.rs b/src/objects/channel.rs new file mode 100644 index 0000000..c9b5f1f --- /dev/null +++ b/src/objects/channel.rs @@ -0,0 +1,353 @@ +use diesel::{ + ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, delete, + insert_into, update, +}; +use diesel_async::{RunQueryDsl, pooled_connection::AsyncDieselConnectionManager}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::{ + Conn, Data, + error::Error, + schema::{channel_permissions, channels, messages}, + utils::{CHANNEL_REGEX, order_by_is_above}, +}; + +use super::{HasIsAbove, HasUuid, Message, load_or_empty, message::MessageBuilder}; + +#[derive(Queryable, Selectable, Insertable, Clone, Debug)] +#[diesel(table_name = channels)] +#[diesel(check_for_backend(diesel::pg::Pg))] +struct ChannelBuilder { + uuid: Uuid, + guild_uuid: Uuid, + name: String, + description: Option, + is_above: Option, +} + +impl ChannelBuilder { + async fn build(self, conn: &mut Conn) -> Result { + use self::channel_permissions::dsl::*; + let channel_permission: Vec = load_or_empty( + channel_permissions + .filter(channel_uuid.eq(self.uuid)) + .select(ChannelPermission::as_select()) + .load(conn) + .await, + )?; + + Ok(Channel { + uuid: self.uuid, + guild_uuid: self.guild_uuid, + name: self.name, + description: self.description, + is_above: self.is_above, + permissions: channel_permission, + }) + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Channel { + pub uuid: Uuid, + pub guild_uuid: Uuid, + name: String, + description: Option, + pub is_above: Option, + pub permissions: Vec, +} + +#[derive(Serialize, Deserialize, Clone, Queryable, Selectable, Debug)] +#[diesel(table_name = channel_permissions)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct ChannelPermission { + pub role_uuid: Uuid, + pub permissions: i64, +} + +impl HasUuid for Channel { + fn uuid(&self) -> &Uuid { + self.uuid.as_ref() + } +} + +impl HasIsAbove for Channel { + fn is_above(&self) -> Option<&Uuid> { + self.is_above.as_ref() + } +} + +impl Channel { + pub async fn fetch_all( + pool: &deadpool::managed::Pool< + AsyncDieselConnectionManager, + Conn, + >, + guild_uuid: Uuid, + ) -> Result, Error> { + let mut conn = pool.get().await?; + + use channels::dsl; + let channel_builders: Vec = load_or_empty( + dsl::channels + .filter(dsl::guild_uuid.eq(guild_uuid)) + .select(ChannelBuilder::as_select()) + .load(&mut conn) + .await, + )?; + + let channel_futures = channel_builders.iter().map(async move |c| { + let mut conn = pool.get().await?; + c.clone().build(&mut conn).await + }); + + futures::future::try_join_all(channel_futures).await + } + + pub async fn fetch_one(data: &Data, channel_uuid: Uuid) -> Result { + if let Ok(cache_hit) = data.get_cache_key(channel_uuid.to_string()).await { + return Ok(serde_json::from_str(&cache_hit)?); + } + + let mut conn = data.pool.get().await?; + + use channels::dsl; + let channel_builder: ChannelBuilder = dsl::channels + .filter(dsl::uuid.eq(channel_uuid)) + .select(ChannelBuilder::as_select()) + .get_result(&mut conn) + .await?; + + let channel = channel_builder.build(&mut conn).await?; + + data.set_cache_key(channel_uuid.to_string(), channel.clone(), 60) + .await?; + + Ok(channel) + } + + pub async fn new( + data: actix_web::web::Data, + guild_uuid: Uuid, + name: String, + description: Option, + ) -> Result { + if !CHANNEL_REGEX.is_match(&name) { + return Err(Error::BadRequest("Channel name is invalid".to_string())); + } + + let mut conn = data.pool.get().await?; + + let channel_uuid = Uuid::now_v7(); + + let channels = Self::fetch_all(&data.pool, guild_uuid).await?; + + let channels_ordered = order_by_is_above(channels).await?; + + let last_channel = channels_ordered.last(); + + let new_channel = ChannelBuilder { + uuid: channel_uuid, + guild_uuid, + name: name.clone(), + description: description.clone(), + is_above: None, + }; + + insert_into(channels::table) + .values(new_channel.clone()) + .execute(&mut conn) + .await?; + + if let Some(old_last_channel) = last_channel { + use channels::dsl; + update(channels::table) + .filter(dsl::uuid.eq(old_last_channel.uuid)) + .set(dsl::is_above.eq(new_channel.uuid)) + .execute(&mut conn) + .await?; + } + + // returns different object because there's no reason to build the channelbuilder (wastes 1 database request) + let channel = Self { + uuid: channel_uuid, + guild_uuid, + name, + description, + is_above: None, + permissions: vec![], + }; + + data.set_cache_key(channel_uuid.to_string(), channel.clone(), 1800) + .await?; + + if data + .get_cache_key(format!("{}_channels", guild_uuid)) + .await + .is_ok() + { + data.del_cache_key(format!("{}_channels", guild_uuid)) + .await?; + } + + Ok(channel) + } + + pub async fn delete(self, data: &Data) -> Result<(), Error> { + let mut conn = data.pool.get().await?; + + use channels::dsl; + delete(channels::table) + .filter(dsl::uuid.eq(self.uuid)) + .execute(&mut conn) + .await?; + + if data.get_cache_key(self.uuid.to_string()).await.is_ok() { + data.del_cache_key(self.uuid.to_string()).await?; + } + + Ok(()) + } + + pub async fn fetch_messages( + &self, + data: &Data, + amount: i64, + offset: i64, + ) -> Result, Error> { + let mut conn = data.pool.get().await?; + + use messages::dsl; + let messages: Vec = load_or_empty( + dsl::messages + .filter(dsl::channel_uuid.eq(self.uuid)) + .select(MessageBuilder::as_select()) + .order(dsl::uuid.desc()) + .limit(amount) + .offset(offset) + .load(&mut conn) + .await, + )?; + + let message_futures = messages.iter().map(async move |b| b.build(data).await); + + futures::future::try_join_all(message_futures).await + } + + pub async fn new_message( + &self, + data: &Data, + user_uuid: Uuid, + message: String, + ) -> Result { + let message_uuid = Uuid::now_v7(); + + let message = MessageBuilder { + uuid: message_uuid, + channel_uuid: self.uuid, + user_uuid, + message, + }; + + let mut conn = data.pool.get().await?; + + insert_into(messages::table) + .values(message.clone()) + .execute(&mut conn) + .await?; + + message.build(data).await + } + + pub async fn set_name(&mut self, data: &Data, new_name: String) -> Result<(), Error> { + if !CHANNEL_REGEX.is_match(&new_name) { + return Err(Error::BadRequest("Channel name is invalid".to_string())); + } + + let mut conn = data.pool.get().await?; + + use channels::dsl; + update(channels::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(dsl::name.eq(&new_name)) + .execute(&mut conn) + .await?; + + self.name = new_name; + + Ok(()) + } + + pub async fn set_description( + &mut self, + data: &Data, + new_description: String, + ) -> Result<(), Error> { + let mut conn = data.pool.get().await?; + + use channels::dsl; + update(channels::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(dsl::description.eq(&new_description)) + .execute(&mut conn) + .await?; + + self.description = Some(new_description); + + Ok(()) + } + + pub async fn move_channel(&mut self, data: &Data, new_is_above: Uuid) -> Result<(), Error> { + let mut conn = data.pool.get().await?; + + use channels::dsl; + let old_above_uuid: Option = match dsl::channels + .filter(dsl::is_above.eq(self.uuid)) + .select(dsl::uuid) + .get_result(&mut conn) + .await + { + Ok(r) => Ok(Some(r)), + Err(e) if e == diesel::result::Error::NotFound => Ok(None), + Err(e) => Err(e), + }?; + + if let Some(uuid) = old_above_uuid { + update(channels::table) + .filter(dsl::uuid.eq(uuid)) + .set(dsl::is_above.eq(None::)) + .execute(&mut conn) + .await?; + } + + match update(channels::table) + .filter(dsl::is_above.eq(new_is_above)) + .set(dsl::is_above.eq(self.uuid)) + .execute(&mut conn) + .await + { + Ok(r) => Ok(r), + Err(e) if e == diesel::result::Error::NotFound => Ok(0), + Err(e) => Err(e), + }?; + + update(channels::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(dsl::is_above.eq(new_is_above)) + .execute(&mut conn) + .await?; + + if let Some(uuid) = old_above_uuid { + update(channels::table) + .filter(dsl::uuid.eq(uuid)) + .set(dsl::is_above.eq(self.is_above)) + .execute(&mut conn) + .await?; + } + + self.is_above = Some(new_is_above); + + Ok(()) + } +} diff --git a/src/objects/email_token.rs b/src/objects/email_token.rs new file mode 100644 index 0000000..e458cf7 --- /dev/null +++ b/src/objects/email_token.rs @@ -0,0 +1,80 @@ +use chrono::Utc; +use diesel::{ + ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper, delete, dsl::now, + insert_into, +}; +use diesel_async::RunQueryDsl; +use lettre::message::MultiPart; +use uuid::Uuid; + +use crate::{Conn, Data, error::Error, schema::email_tokens, utils::generate_refresh_token}; + +use super::Me; + +#[derive(Selectable, Queryable)] +#[diesel(table_name = email_tokens)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct EmailToken { + user_uuid: Uuid, + pub token: String, + pub created_at: chrono::DateTime, +} + +impl EmailToken { + pub async fn get(conn: &mut Conn, user_uuid: Uuid) -> Result { + use email_tokens::dsl; + let email_token = dsl::email_tokens + .filter(dsl::user_uuid.eq(user_uuid)) + .select(EmailToken::as_select()) + .get_result(conn) + .await?; + + Ok(email_token) + } + + #[allow(clippy::new_ret_no_self)] + pub async fn new(data: &Data, me: Me) -> Result<(), Error> { + let token = generate_refresh_token()?; + + let mut conn = data.pool.get().await?; + + use email_tokens::dsl; + insert_into(email_tokens::table) + .values(( + dsl::user_uuid.eq(me.uuid), + dsl::token.eq(&token), + dsl::created_at.eq(now), + )) + .execute(&mut conn) + .await?; + + let mut verify_endpoint = data.config.web.frontend_url.join("verify-email")?; + + verify_endpoint.set_query(Some(&format!("token={}", token))); + + let email = data + .mail_client + .message_builder() + .to(me.email.parse()?) + .subject(format!("{} E-mail Verification", data.config.instance.name)) + .multipart(MultiPart::alternative_plain_html( + format!("Verify your {} account\n\nHello, {}!\nThanks for creating a new account on Gorb.\nThe final step to create your account is to verify your email address by visiting the page, within 24 hours.\n\n{}\n\nIf you didn't ask to verify this address, you can safely ignore this email\n\nThanks, The gorb team.", data.config.instance.name, me.username, verify_endpoint), + format!(r#"

Verify your {} Account

Hello, {}!

Thanks for creating a new account on Gorb.

The final step to create your account is to verify your email address by clicking the button below, within 24 hours.

VERIFY ACCOUNT

If you didn't ask to verify this address, you can safely ignore this email.

"#, data.config.instance.name, me.username, verify_endpoint) + ))?; + + data.mail_client.send_mail(email).await?; + + Ok(()) + } + + pub async fn delete(&self, conn: &mut Conn) -> Result<(), Error> { + use email_tokens::dsl; + delete(email_tokens::table) + .filter(dsl::user_uuid.eq(self.user_uuid)) + .filter(dsl::token.eq(&self.token)) + .execute(conn) + .await?; + + Ok(()) + } +} diff --git a/src/objects/guild.rs b/src/objects/guild.rs new file mode 100644 index 0000000..f5e973d --- /dev/null +++ b/src/objects/guild.rs @@ -0,0 +1,226 @@ +use actix_web::web::BytesMut; +use diesel::{ + ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, insert_into, + update, +}; +use diesel_async::{RunQueryDsl, pooled_connection::AsyncDieselConnectionManager}; +use serde::Serialize; +use tokio::task; +use url::Url; +use uuid::Uuid; + +use crate::{ + Conn, + error::Error, + schema::{guild_members, guilds, invites}, + utils::image_check, +}; + +use super::{Invite, Member, Role, load_or_empty, member::MemberBuilder}; + +#[derive(Serialize, Queryable, Selectable, Insertable, Clone)] +#[diesel(table_name = guilds)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct GuildBuilder { + uuid: Uuid, + name: String, + description: Option, + icon: Option, + owner_uuid: Uuid, +} + +impl GuildBuilder { + pub async fn build(self, conn: &mut Conn) -> Result { + let member_count = Member::count(conn, self.uuid).await?; + + let roles = Role::fetch_all(conn, self.uuid).await?; + + Ok(Guild { + uuid: self.uuid, + name: self.name, + description: self.description, + icon: self.icon.and_then(|i| i.parse().ok()), + owner_uuid: self.owner_uuid, + roles, + member_count, + }) + } +} + +#[derive(Serialize)] +pub struct Guild { + pub uuid: Uuid, + name: String, + description: Option, + icon: Option, + owner_uuid: Uuid, + pub roles: Vec, + member_count: i64, +} + +impl Guild { + pub async fn fetch_one(conn: &mut Conn, guild_uuid: Uuid) -> Result { + use guilds::dsl; + let guild_builder: GuildBuilder = dsl::guilds + .filter(dsl::uuid.eq(guild_uuid)) + .select(GuildBuilder::as_select()) + .get_result(conn) + .await?; + + guild_builder.build(conn).await + } + + pub async fn fetch_amount( + pool: &deadpool::managed::Pool< + AsyncDieselConnectionManager, + Conn, + >, + offset: i64, + amount: i64, + ) -> Result, Error> { + // Fetch guild data from database + let mut conn = pool.get().await?; + + use guilds::dsl; + let guild_builders: Vec = load_or_empty( + dsl::guilds + .select(GuildBuilder::as_select()) + .order_by(dsl::uuid) + .offset(offset) + .limit(amount) + .load(&mut conn) + .await, + )?; + + // Process each guild concurrently + let guild_futures = guild_builders.iter().map(async move |g| { + let mut conn = pool.get().await?; + g.clone().build(&mut conn).await + }); + + // Execute all futures concurrently and collect results + futures::future::try_join_all(guild_futures).await + } + + pub async fn new(conn: &mut Conn, name: String, owner_uuid: Uuid) -> Result { + let guild_uuid = Uuid::now_v7(); + + let guild_builder = GuildBuilder { + uuid: guild_uuid, + name: name.clone(), + description: None, + icon: None, + owner_uuid, + }; + + insert_into(guilds::table) + .values(guild_builder) + .execute(conn) + .await?; + + let member_uuid = Uuid::now_v7(); + + let member = MemberBuilder { + uuid: member_uuid, + nickname: None, + user_uuid: owner_uuid, + guild_uuid, + }; + + insert_into(guild_members::table) + .values(member) + .execute(conn) + .await?; + + Ok(Guild { + uuid: guild_uuid, + name, + description: None, + icon: None, + owner_uuid, + roles: vec![], + member_count: 1, + }) + } + + pub async fn get_invites(&self, conn: &mut Conn) -> Result, Error> { + use invites::dsl; + let invites = load_or_empty( + dsl::invites + .filter(dsl::guild_uuid.eq(self.uuid)) + .select(Invite::as_select()) + .load(conn) + .await, + )?; + + Ok(invites) + } + + pub async fn create_invite( + &self, + conn: &mut Conn, + user_uuid: Uuid, + custom_id: Option, + ) -> Result { + let invite_id; + + if let Some(id) = custom_id { + invite_id = id; + if invite_id.len() > 32 { + return Err(Error::BadRequest("MAX LENGTH".to_string())); + } + } else { + let charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + + invite_id = random_string::generate(8, charset); + } + + let invite = Invite { + id: invite_id, + user_uuid, + guild_uuid: self.uuid, + }; + + insert_into(invites::table) + .values(invite.clone()) + .execute(conn) + .await?; + + Ok(invite) + } + + // FIXME: Horrible security + pub async fn set_icon( + &mut self, + bunny_cdn: &bunny_api_tokio::Client, + conn: &mut Conn, + cdn_url: Url, + icon: BytesMut, + ) -> Result<(), Error> { + let icon_clone = icon.clone(); + let image_type = task::spawn_blocking(move || image_check(icon_clone)).await??; + + if let Some(icon) = &self.icon { + let relative_url = icon.path().trim_start_matches('/'); + + bunny_cdn.storage.delete(relative_url).await?; + } + + let path = format!("icons/{}/icon.{}", self.uuid, image_type); + + bunny_cdn.storage.upload(path.clone(), icon.into()).await?; + + let icon_url = cdn_url.join(&path)?; + + use guilds::dsl; + update(guilds::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(dsl::icon.eq(icon_url.as_str())) + .execute(conn) + .await?; + + self.icon = Some(icon_url); + + Ok(()) + } +} diff --git a/src/objects/invite.rs b/src/objects/invite.rs new file mode 100644 index 0000000..5e0827e --- /dev/null +++ b/src/objects/invite.rs @@ -0,0 +1,30 @@ +use diesel::{ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper}; +use diesel_async::RunQueryDsl; +use serde::Serialize; +use uuid::Uuid; + +use crate::{Conn, error::Error, schema::invites}; + +/// Server invite struct +#[derive(Clone, Serialize, Queryable, Selectable, Insertable)] +pub struct Invite { + /// case-sensitive alphanumeric string with a fixed length of 8 characters, can be up to 32 characters for custom invites + pub id: String, + /// User that created the invite + pub user_uuid: Uuid, + /// UUID of the guild that the invite belongs to + pub guild_uuid: Uuid, +} + +impl Invite { + pub async fn fetch_one(conn: &mut Conn, invite_id: String) -> Result { + use invites::dsl; + let invite: Invite = dsl::invites + .filter(dsl::id.eq(invite_id)) + .select(Invite::as_select()) + .get_result(conn) + .await?; + + Ok(invite) + } +} diff --git a/src/objects/me.rs b/src/objects/me.rs new file mode 100644 index 0000000..6af5bce --- /dev/null +++ b/src/objects/me.rs @@ -0,0 +1,232 @@ +use actix_web::web::BytesMut; +use diesel::{ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper, update}; +use diesel_async::RunQueryDsl; +use serde::Serialize; +use tokio::task; +use url::Url; +use uuid::Uuid; + +use crate::{ + Conn, Data, + error::Error, + schema::{guild_members, guilds, users}, + utils::{EMAIL_REGEX, USERNAME_REGEX, image_check}, +}; + +use super::{Guild, guild::GuildBuilder, load_or_empty, member::MemberBuilder}; + +#[derive(Serialize, Queryable, Selectable)] +#[diesel(table_name = users)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct Me { + pub uuid: Uuid, + pub username: String, + pub display_name: Option, + avatar: Option, + pronouns: Option, + about: Option, + pub email: String, + pub email_verified: bool, +} + +impl Me { + pub async fn get(conn: &mut Conn, user_uuid: Uuid) -> Result { + use users::dsl; + let me: Me = dsl::users + .filter(dsl::uuid.eq(user_uuid)) + .select(Me::as_select()) + .get_result(conn) + .await?; + + Ok(me) + } + + pub async fn fetch_memberships(&self, conn: &mut Conn) -> Result, Error> { + use guild_members::dsl; + let memberships: Vec = load_or_empty( + dsl::guild_members + .filter(dsl::user_uuid.eq(self.uuid)) + .select(MemberBuilder::as_select()) + .load(conn) + .await, + )?; + + let mut guilds: Vec = vec![]; + + for membership in memberships { + use guilds::dsl; + guilds.push( + dsl::guilds + .filter(dsl::uuid.eq(membership.guild_uuid)) + .select(GuildBuilder::as_select()) + .get_result(conn) + .await? + .build(conn) + .await?, + ) + } + + Ok(guilds) + } + + pub async fn set_avatar( + &mut self, + data: &Data, + cdn_url: Url, + avatar: BytesMut, + ) -> Result<(), Error> { + let avatar_clone = avatar.clone(); + let image_type = task::spawn_blocking(move || image_check(avatar_clone)).await??; + + let mut conn = data.pool.get().await?; + + if let Some(avatar) = &self.avatar { + let avatar_url: Url = avatar.parse()?; + + let relative_url = avatar_url.path().trim_start_matches('/'); + + data.bunny_cdn.storage.delete(relative_url).await?; + } + + let path = format!("avatar/{}/avatar.{}", self.uuid, image_type); + + data.bunny_cdn + .storage + .upload(path.clone(), avatar.into()) + .await?; + + let avatar_url = cdn_url.join(&path)?; + + use users::dsl; + update(users::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(dsl::avatar.eq(avatar_url.as_str())) + .execute(&mut conn) + .await?; + + if data.get_cache_key(self.uuid.to_string()).await.is_ok() { + data.del_cache_key(self.uuid.to_string()).await? + } + + self.avatar = Some(avatar_url.to_string()); + + Ok(()) + } + + pub async fn verify_email(&self, conn: &mut Conn) -> Result<(), Error> { + use users::dsl; + update(users::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(dsl::email_verified.eq(true)) + .execute(conn) + .await?; + + Ok(()) + } + + pub async fn set_username(&mut self, data: &Data, new_username: String) -> Result<(), Error> { + if !USERNAME_REGEX.is_match(&new_username) { + return Err(Error::BadRequest("Invalid username".to_string())); + } + + let mut conn = data.pool.get().await?; + + use users::dsl; + update(users::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(dsl::username.eq(new_username.as_str())) + .execute(&mut conn) + .await?; + + if data.get_cache_key(self.uuid.to_string()).await.is_ok() { + data.del_cache_key(self.uuid.to_string()).await? + } + + self.username = new_username; + + Ok(()) + } + + pub async fn set_display_name( + &mut self, + data: &Data, + new_display_name: String, + ) -> Result<(), Error> { + let mut conn = data.pool.get().await?; + + use users::dsl; + update(users::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(dsl::display_name.eq(new_display_name.as_str())) + .execute(&mut conn) + .await?; + + if data.get_cache_key(self.uuid.to_string()).await.is_ok() { + data.del_cache_key(self.uuid.to_string()).await? + } + + self.display_name = Some(new_display_name); + + Ok(()) + } + + pub async fn set_email(&mut self, data: &Data, new_email: String) -> Result<(), Error> { + if !EMAIL_REGEX.is_match(&new_email) { + return Err(Error::BadRequest("Invalid username".to_string())); + } + + let mut conn = data.pool.get().await?; + + use users::dsl; + update(users::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(( + dsl::email.eq(new_email.as_str()), + dsl::email_verified.eq(false), + )) + .execute(&mut conn) + .await?; + + if data.get_cache_key(self.uuid.to_string()).await.is_ok() { + data.del_cache_key(self.uuid.to_string()).await? + } + + self.email = new_email; + + Ok(()) + } + + pub async fn set_pronouns(&mut self, data: &Data, new_pronouns: String) -> Result<(), Error> { + let mut conn = data.pool.get().await?; + + use users::dsl; + update(users::table) + .filter(dsl::uuid.eq(self.uuid)) + .set((dsl::pronouns.eq(new_pronouns.as_str()),)) + .execute(&mut conn) + .await?; + + if data.get_cache_key(self.uuid.to_string()).await.is_ok() { + data.del_cache_key(self.uuid.to_string()).await? + } + + Ok(()) + } + + pub async fn set_about(&mut self, data: &Data, new_about: String) -> Result<(), Error> { + let mut conn = data.pool.get().await?; + + use users::dsl; + update(users::table) + .filter(dsl::uuid.eq(self.uuid)) + .set((dsl::about.eq(new_about.as_str()),)) + .execute(&mut conn) + .await?; + + if data.get_cache_key(self.uuid.to_string()).await.is_ok() { + data.del_cache_key(self.uuid.to_string()).await? + } + + Ok(()) + } +} diff --git a/src/objects/member.rs b/src/objects/member.rs new file mode 100644 index 0000000..f18e726 --- /dev/null +++ b/src/objects/member.rs @@ -0,0 +1,125 @@ +use diesel::{ + ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, insert_into, +}; +use diesel_async::RunQueryDsl; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::{Conn, Data, error::Error, schema::guild_members}; + +use super::{User, load_or_empty}; + +#[derive(Serialize, Queryable, Selectable, Insertable)] +#[diesel(table_name = guild_members)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct MemberBuilder { + pub uuid: Uuid, + pub nickname: Option, + pub user_uuid: Uuid, + pub guild_uuid: Uuid, +} + +impl MemberBuilder { + async fn build(&self, data: &Data) -> Result { + let user = User::fetch_one(data, self.user_uuid).await?; + + Ok(Member { + uuid: self.uuid, + nickname: self.nickname.clone(), + user_uuid: self.user_uuid, + guild_uuid: self.guild_uuid, + user, + }) + } +} + +#[derive(Serialize, Deserialize)] +pub struct Member { + pub uuid: Uuid, + pub nickname: Option, + pub user_uuid: Uuid, + pub guild_uuid: Uuid, + user: User, +} + +impl Member { + pub async fn count(conn: &mut Conn, guild_uuid: Uuid) -> Result { + use guild_members::dsl; + let count: i64 = dsl::guild_members + .filter(dsl::guild_uuid.eq(guild_uuid)) + .count() + .get_result(conn) + .await?; + + Ok(count) + } + + pub async fn check_membership( + conn: &mut Conn, + user_uuid: Uuid, + guild_uuid: Uuid, + ) -> Result<(), Error> { + use guild_members::dsl; + dsl::guild_members + .filter(dsl::user_uuid.eq(user_uuid)) + .filter(dsl::guild_uuid.eq(guild_uuid)) + .select(MemberBuilder::as_select()) + .get_result(conn) + .await?; + + Ok(()) + } + + pub async fn fetch_one(data: &Data, user_uuid: Uuid, guild_uuid: Uuid) -> Result { + let mut conn = data.pool.get().await?; + + use guild_members::dsl; + let member: MemberBuilder = dsl::guild_members + .filter(dsl::user_uuid.eq(user_uuid)) + .filter(dsl::guild_uuid.eq(guild_uuid)) + .select(MemberBuilder::as_select()) + .get_result(&mut conn) + .await?; + + member.build(data).await + } + + pub async fn fetch_all(data: &Data, guild_uuid: Uuid) -> Result, Error> { + let mut conn = data.pool.get().await?; + + use guild_members::dsl; + let member_builders: Vec = load_or_empty( + dsl::guild_members + .filter(dsl::guild_uuid.eq(guild_uuid)) + .select(MemberBuilder::as_select()) + .load(&mut conn) + .await, + )?; + + let member_futures = member_builders + .iter() + .map(async move |m| m.build(data).await); + + futures::future::try_join_all(member_futures).await + } + + pub async fn new(data: &Data, user_uuid: Uuid, guild_uuid: Uuid) -> Result { + let mut conn = data.pool.get().await?; + + let member_uuid = Uuid::now_v7(); + + let member = MemberBuilder { + uuid: member_uuid, + guild_uuid, + user_uuid, + nickname: None, + }; + + insert_into(guild_members::table) + .values(&member) + .execute(&mut conn) + .await?; + + member.build(data).await + } +} diff --git a/src/objects/message.rs b/src/objects/message.rs new file mode 100644 index 0000000..6c1700a --- /dev/null +++ b/src/objects/message.rs @@ -0,0 +1,40 @@ +use diesel::{Insertable, Queryable, Selectable}; +use serde::Serialize; +use uuid::Uuid; + +use crate::{Data, error::Error, schema::messages}; + +use super::User; + +#[derive(Clone, Queryable, Selectable, Insertable)] +#[diesel(table_name = messages)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct MessageBuilder { + pub uuid: Uuid, + pub channel_uuid: Uuid, + pub user_uuid: Uuid, + pub message: String, +} + +impl MessageBuilder { + pub async fn build(&self, data: &Data) -> Result { + let user = User::fetch_one(data, self.user_uuid).await?; + + Ok(Message { + uuid: self.uuid, + channel_uuid: self.channel_uuid, + user_uuid: self.user_uuid, + message: self.message.clone(), + user, + }) + } +} + +#[derive(Clone, Serialize)] +pub struct Message { + uuid: Uuid, + channel_uuid: Uuid, + user_uuid: Uuid, + message: String, + user: User, +} diff --git a/src/objects/mod.rs b/src/objects/mod.rs new file mode 100644 index 0000000..7b45957 --- /dev/null +++ b/src/objects/mod.rs @@ -0,0 +1,156 @@ +use lettre::{ + AsyncSmtpTransport, AsyncTransport, Message as Email, Tokio1Executor, + message::{Mailbox, MessageBuilder as EmailBuilder}, + transport::smtp::authentication::Credentials, +}; +use log::debug; +use serde::Deserialize; +use uuid::Uuid; + +mod channel; +mod email_token; +mod guild; +mod invite; +mod me; +mod member; +mod message; +mod password_reset_token; +mod role; +mod user; + +pub use channel::Channel; +pub use email_token::EmailToken; +pub use guild::Guild; +pub use invite::Invite; +pub use me::Me; +pub use member::Member; +pub use message::Message; +pub use password_reset_token::PasswordResetToken; +pub use role::Role; +pub use user::User; + +use crate::error::Error; + +pub trait HasUuid { + fn uuid(&self) -> &Uuid; +} + +pub trait HasIsAbove { + fn is_above(&self) -> Option<&Uuid>; +} + +fn load_or_empty( + query_result: Result, diesel::result::Error>, +) -> Result, diesel::result::Error> { + match query_result { + Ok(vec) => Ok(vec), + Err(diesel::result::Error::NotFound) => Ok(Vec::new()), + Err(e) => Err(e), + } +} + +#[derive(PartialEq, Eq, Clone)] +pub enum MailTls { + StartTls, + Tls, +} + +impl From for MailTls { + fn from(value: String) -> Self { + match &*value.to_lowercase() { + "starttls" => Self::StartTls, + _ => Self::Tls, + } + } +} + +#[derive(Clone)] +pub struct MailClient { + creds: Credentials, + smtp_server: String, + mbox: Mailbox, + tls: MailTls, +} + +impl MailClient { + pub fn new>( + creds: Credentials, + smtp_server: String, + mbox: String, + tls: T, + ) -> Result { + Ok(Self { + creds, + smtp_server, + mbox: mbox.parse()?, + tls: tls.into(), + }) + } + + pub fn message_builder(&self) -> EmailBuilder { + Email::builder().from(self.mbox.clone()) + } + + pub async fn send_mail(&self, email: Email) -> Result<(), Error> { + let mailer: AsyncSmtpTransport = match self.tls { + MailTls::StartTls => { + AsyncSmtpTransport::::starttls_relay(&self.smtp_server)? + .credentials(self.creds.clone()) + .build() + } + MailTls::Tls => AsyncSmtpTransport::::relay(&self.smtp_server)? + .credentials(self.creds.clone()) + .build(), + }; + + let response = mailer.send(email).await?; + + debug!("mail sending response: {:?}", response); + + Ok(()) + } +} + +#[derive(Clone, Copy)] +pub enum Permissions { + SendMessage = 1, + CreateChannel = 2, + DeleteChannel = 4, + ManageChannel = 8, + CreateRole = 16, + DeleteRole = 32, + ManageRole = 64, + CreateInvite = 128, + ManageInvite = 256, + ManageServer = 512, + ManageMember = 1024, +} + +impl Permissions { + pub fn fetch_permissions(permissions: i64) -> Vec { + let all_perms = vec![ + Self::SendMessage, + Self::CreateChannel, + Self::DeleteChannel, + Self::ManageChannel, + Self::CreateRole, + Self::DeleteRole, + Self::ManageRole, + Self::CreateInvite, + Self::ManageInvite, + Self::ManageServer, + Self::ManageMember, + ]; + + all_perms + .into_iter() + .filter(|p| permissions & (*p as i64) != 0) + .collect() + } +} + +#[derive(Deserialize)] +pub struct StartAmountQuery { + pub start: Option, + pub amount: Option, +} diff --git a/src/objects/password_reset_token.rs b/src/objects/password_reset_token.rs new file mode 100644 index 0000000..e3c7bca --- /dev/null +++ b/src/objects/password_reset_token.rs @@ -0,0 +1,160 @@ +use argon2::{ + PasswordHasher, + password_hash::{SaltString, rand_core::OsRng}, +}; +use chrono::Utc; +use diesel::{ + ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper, delete, dsl::now, + insert_into, update, +}; +use diesel_async::RunQueryDsl; +use lettre::message::MultiPart; +use uuid::Uuid; + +use crate::{ + Conn, Data, + error::Error, + schema::{password_reset_tokens, users}, + utils::{PASSWORD_REGEX, generate_refresh_token, global_checks, user_uuid_from_identifier}, +}; + +#[derive(Selectable, Queryable)] +#[diesel(table_name = password_reset_tokens)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct PasswordResetToken { + user_uuid: Uuid, + pub token: String, + pub created_at: chrono::DateTime, +} + +impl PasswordResetToken { + pub async fn get(conn: &mut Conn, token: String) -> Result { + use password_reset_tokens::dsl; + let password_reset_token = dsl::password_reset_tokens + .filter(dsl::token.eq(token)) + .select(PasswordResetToken::as_select()) + .get_result(conn) + .await?; + + Ok(password_reset_token) + } + + pub async fn get_with_identifier( + conn: &mut Conn, + identifier: String, + ) -> Result { + let user_uuid = user_uuid_from_identifier(conn, &identifier).await?; + + use password_reset_tokens::dsl; + let password_reset_token = dsl::password_reset_tokens + .filter(dsl::user_uuid.eq(user_uuid)) + .select(PasswordResetToken::as_select()) + .get_result(conn) + .await?; + + Ok(password_reset_token) + } + + #[allow(clippy::new_ret_no_self)] + pub async fn new(data: &Data, identifier: String) -> Result<(), Error> { + let token = generate_refresh_token()?; + + let mut conn = data.pool.get().await?; + + let user_uuid = user_uuid_from_identifier(&mut conn, &identifier).await?; + + global_checks(data, user_uuid).await?; + + use users::dsl as udsl; + let (username, email_address): (String, String) = udsl::users + .filter(udsl::uuid.eq(user_uuid)) + .select((udsl::username, udsl::email)) + .get_result(&mut conn) + .await?; + + use password_reset_tokens::dsl; + insert_into(password_reset_tokens::table) + .values(( + dsl::user_uuid.eq(user_uuid), + dsl::token.eq(&token), + dsl::created_at.eq(now), + )) + .execute(&mut conn) + .await?; + + let mut reset_endpoint = data.config.web.frontend_url.join("reset-password")?; + + reset_endpoint.set_query(Some(&format!("token={}", token))); + + let email = data + .mail_client + .message_builder() + .to(email_address.parse()?) + .subject(format!("{} Password Reset", data.config.instance.name)) + .multipart(MultiPart::alternative_plain_html( + format!("{} Password Reset\n\nHello, {}!\nSomeone requested a password reset for your Gorb account.\nClick the button below within 24 hours to reset your password.\n\n{}\n\nIf you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.\n\nThanks, The gorb team.", data.config.instance.name, username, reset_endpoint), + format!(r#"

{} Password Reset

Hello, {}!

Someone requested a password reset for your Gorb account.

Click the button below within 24 hours to reset your password.

RESET PASSWORD

If you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.

"#, data.config.instance.name, username, reset_endpoint) + ))?; + + data.mail_client.send_mail(email).await?; + + Ok(()) + } + + pub async fn set_password(&self, data: &Data, password: String) -> Result<(), Error> { + if !PASSWORD_REGEX.is_match(&password) { + return Err(Error::BadRequest( + "Please provide a valid password".to_string(), + )); + } + + let salt = SaltString::generate(&mut OsRng); + + let hashed_password = data + .argon2 + .hash_password(password.as_bytes(), &salt) + .map_err(|e| Error::PasswordHashError(e.to_string()))?; + + let mut conn = data.pool.get().await?; + + use users::dsl; + update(users::table) + .filter(dsl::uuid.eq(self.user_uuid)) + .set(dsl::password.eq(hashed_password.to_string())) + .execute(&mut conn) + .await?; + + let (username, email_address): (String, String) = dsl::users + .filter(dsl::uuid.eq(self.user_uuid)) + .select((dsl::username, dsl::email)) + .get_result(&mut conn) + .await?; + + let login_page = data.config.web.frontend_url.join("login")?; + + let email = data + .mail_client + .message_builder() + .to(email_address.parse()?) + .subject(format!("Your {} Password has been Reset", data.config.instance.name)) + .multipart(MultiPart::alternative_plain_html( + format!("{} Password Reset Confirmation\n\nHello, {}!\nYour password has been successfully reset for your Gorb account.\nIf you did not initiate this change, please click the link below to reset your password immediately.\n\n{}\n\nThanks, The gorb team.", data.config.instance.name, username, login_page), + format!(r#"

{} Password Reset Confirmation

Hello, {}!

Your password has been successfully reset for your Gorb account.

If you did not initiate this change, please click the button below to reset your password immediately.

RESET PASSWORD
"#, data.config.instance.name, username, login_page) + ))?; + + data.mail_client.send_mail(email).await?; + + self.delete(&mut conn).await + } + + pub async fn delete(&self, conn: &mut Conn) -> Result<(), Error> { + use password_reset_tokens::dsl; + delete(password_reset_tokens::table) + .filter(dsl::user_uuid.eq(self.user_uuid)) + .filter(dsl::token.eq(&self.token)) + .execute(conn) + .await?; + + Ok(()) + } +} diff --git a/src/objects/role.rs b/src/objects/role.rs new file mode 100644 index 0000000..f67dc6d --- /dev/null +++ b/src/objects/role.rs @@ -0,0 +1,96 @@ +use diesel::{ + ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, insert_into, + update, +}; +use diesel_async::RunQueryDsl; +use serde::Serialize; +use uuid::Uuid; + +use crate::{Conn, error::Error, schema::roles, utils::order_by_is_above}; + +use super::{HasIsAbove, HasUuid, load_or_empty}; + +#[derive(Serialize, Clone, Queryable, Selectable, Insertable)] +#[diesel(table_name = roles)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct Role { + uuid: Uuid, + guild_uuid: Uuid, + name: String, + color: i32, + is_above: Option, + permissions: i64, +} + +impl HasUuid for Role { + fn uuid(&self) -> &Uuid { + self.uuid.as_ref() + } +} + +impl HasIsAbove for Role { + fn is_above(&self) -> Option<&Uuid> { + self.is_above.as_ref() + } +} + +impl Role { + pub async fn fetch_all(conn: &mut Conn, guild_uuid: Uuid) -> Result, Error> { + use roles::dsl; + let roles: Vec = load_or_empty( + dsl::roles + .filter(dsl::guild_uuid.eq(guild_uuid)) + .select(Role::as_select()) + .load(conn) + .await, + )?; + + Ok(roles) + } + + pub async fn fetch_one(conn: &mut Conn, role_uuid: Uuid) -> Result { + use roles::dsl; + let role: Role = dsl::roles + .filter(dsl::uuid.eq(role_uuid)) + .select(Role::as_select()) + .get_result(conn) + .await?; + + Ok(role) + } + + pub async fn new(conn: &mut Conn, guild_uuid: Uuid, name: String) -> Result { + let role_uuid = Uuid::now_v7(); + + let roles = Self::fetch_all(conn, guild_uuid).await?; + + let roles_ordered = order_by_is_above(roles).await?; + + let last_role = roles_ordered.last(); + + let new_role = Role { + uuid: role_uuid, + guild_uuid, + name, + color: 16777215, + is_above: None, + permissions: 0, + }; + + insert_into(roles::table) + .values(new_role.clone()) + .execute(conn) + .await?; + + if let Some(old_last_role) = last_role { + use roles::dsl; + update(roles::table) + .filter(dsl::uuid.eq(old_last_role.uuid)) + .set(dsl::is_above.eq(new_role.uuid)) + .execute(conn) + .await?; + } + + Ok(new_role) + } +} diff --git a/src/objects/user.rs b/src/objects/user.rs new file mode 100644 index 0000000..98e5e80 --- /dev/null +++ b/src/objects/user.rs @@ -0,0 +1,60 @@ +use diesel::{ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper}; +use diesel_async::RunQueryDsl; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::{Conn, Data, error::Error, schema::users}; + +use super::load_or_empty; + +#[derive(Deserialize, Serialize, Clone, Queryable, Selectable)] +#[diesel(table_name = users)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct User { + uuid: Uuid, + username: String, + display_name: Option, + avatar: Option, + pronouns: Option, + about: Option, +} + +impl User { + pub async fn fetch_one(data: &Data, user_uuid: Uuid) -> Result { + let mut conn = data.pool.get().await?; + + if let Ok(cache_hit) = data.get_cache_key(user_uuid.to_string()).await { + return Ok(serde_json::from_str(&cache_hit)?); + } + + use users::dsl; + let user: User = dsl::users + .filter(dsl::uuid.eq(user_uuid)) + .select(User::as_select()) + .get_result(&mut conn) + .await?; + + data.set_cache_key(user_uuid.to_string(), user.clone(), 1800) + .await?; + + Ok(user) + } + + pub async fn fetch_amount( + conn: &mut Conn, + offset: i64, + amount: i64, + ) -> Result, Error> { + use users::dsl; + let users: Vec = load_or_empty( + dsl::users + .limit(amount) + .offset(offset) + .select(User::as_select()) + .load(conn) + .await, + )?; + + Ok(users) + } +} diff --git a/src/structs.rs b/src/structs.rs deleted file mode 100644 index b68aaf5..0000000 --- a/src/structs.rs +++ /dev/null @@ -1,1437 +0,0 @@ -use actix_web::web::BytesMut; -use argon2::{ - PasswordHasher, - password_hash::{SaltString, rand_core::OsRng}, -}; -use chrono::Utc; -use diesel::{ - ExpressionMethods, QueryDsl, Selectable, SelectableHelper, delete, - dsl::now, - insert_into, - prelude::{Insertable, Queryable}, - update, -}; -use diesel_async::{RunQueryDsl, pooled_connection::AsyncDieselConnectionManager}; -use lettre::{ - AsyncSmtpTransport, AsyncTransport, Message as Email, Tokio1Executor, - message::{Mailbox, MessageBuilder as EmailBuilder, MultiPart}, - transport::smtp::authentication::Credentials, -}; -use log::debug; -use serde::{Deserialize, Serialize}; -use tokio::task; -use url::Url; -use uuid::Uuid; - -use crate::{ - error::Error, schema::*, utils::{ - generate_refresh_token, global_checks, image_check, order_by_is_above, user_uuid_from_identifier, CHANNEL_REGEX, EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX - }, Conn, Data -}; - -pub trait HasUuid { - fn uuid(&self) -> &Uuid; -} - -pub trait HasIsAbove { - fn is_above(&self) -> Option<&Uuid>; -} - -fn load_or_empty( - query_result: Result, diesel::result::Error>, -) -> Result, diesel::result::Error> { - match query_result { - Ok(vec) => Ok(vec), - Err(diesel::result::Error::NotFound) => Ok(Vec::new()), - Err(e) => Err(e), - } -} - -#[derive(PartialEq, Eq, Clone)] -pub enum MailTls { - StartTls, - Tls, -} - -impl From for MailTls { - fn from(value: String) -> Self { - match &*value.to_lowercase() { - "starttls" => Self::StartTls, - _ => Self::Tls, - } - } -} - -#[derive(Clone)] -pub struct MailClient { - creds: Credentials, - smtp_server: String, - mbox: Mailbox, - tls: MailTls, -} - -impl MailClient { - pub fn new>( - creds: Credentials, - smtp_server: String, - mbox: String, - tls: T, - ) -> Result { - Ok(Self { - creds, - smtp_server, - mbox: mbox.parse()?, - tls: tls.into(), - }) - } - - pub fn message_builder(&self) -> EmailBuilder { - Email::builder().from(self.mbox.clone()) - } - - pub async fn send_mail(&self, email: Email) -> Result<(), Error> { - let mailer: AsyncSmtpTransport = match self.tls { - MailTls::StartTls => { - AsyncSmtpTransport::::starttls_relay(&self.smtp_server)? - .credentials(self.creds.clone()) - .build() - } - MailTls::Tls => AsyncSmtpTransport::::relay(&self.smtp_server)? - .credentials(self.creds.clone()) - .build(), - }; - - let response = mailer.send(email).await?; - - debug!("mail sending response: {:?}", response); - - Ok(()) - } -} - -#[derive(Queryable, Selectable, Insertable, Clone, Debug)] -#[diesel(table_name = channels)] -#[diesel(check_for_backend(diesel::pg::Pg))] -struct ChannelBuilder { - uuid: Uuid, - guild_uuid: Uuid, - name: String, - description: Option, - is_above: Option, -} - -impl ChannelBuilder { - async fn build(self, conn: &mut Conn) -> Result { - use self::channel_permissions::dsl::*; - let channel_permission: Vec = load_or_empty( - channel_permissions - .filter(channel_uuid.eq(self.uuid)) - .select(ChannelPermission::as_select()) - .load(conn) - .await, - )?; - - Ok(Channel { - uuid: self.uuid, - guild_uuid: self.guild_uuid, - name: self.name, - description: self.description, - is_above: self.is_above, - permissions: channel_permission, - }) - } -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct Channel { - pub uuid: Uuid, - pub guild_uuid: Uuid, - name: String, - description: Option, - pub is_above: Option, - pub permissions: Vec, -} - -#[derive(Serialize, Deserialize, Clone, Queryable, Selectable, Debug)] -#[diesel(table_name = channel_permissions)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct ChannelPermission { - pub role_uuid: Uuid, - pub permissions: i64, -} - -impl HasUuid for Channel { - fn uuid(&self) -> &Uuid { - self.uuid.as_ref() - } -} - -impl HasIsAbove for Channel { - fn is_above(&self) -> Option<&Uuid> { - self.is_above.as_ref() - } -} - -impl Channel { - pub async fn fetch_all( - pool: &deadpool::managed::Pool< - AsyncDieselConnectionManager, - Conn, - >, - guild_uuid: Uuid, - ) -> Result, Error> { - let mut conn = pool.get().await?; - - use channels::dsl; - let channel_builders: Vec = load_or_empty( - dsl::channels - .filter(dsl::guild_uuid.eq(guild_uuid)) - .select(ChannelBuilder::as_select()) - .load(&mut conn) - .await, - )?; - - let channel_futures = channel_builders.iter().map(async move |c| { - let mut conn = pool.get().await?; - c.clone().build(&mut conn).await - }); - - futures::future::try_join_all(channel_futures).await - } - - pub async fn fetch_one(data: &Data, channel_uuid: Uuid) -> Result { - if let Ok(cache_hit) = data.get_cache_key(channel_uuid.to_string()).await { - return Ok(serde_json::from_str(&cache_hit)?); - } - - let mut conn = data.pool.get().await?; - - use channels::dsl; - let channel_builder: ChannelBuilder = dsl::channels - .filter(dsl::uuid.eq(channel_uuid)) - .select(ChannelBuilder::as_select()) - .get_result(&mut conn) - .await?; - - let channel = channel_builder.build(&mut conn).await?; - - data.set_cache_key(channel_uuid.to_string(), channel.clone(), 60) - .await?; - - Ok(channel) - } - - pub async fn new( - data: actix_web::web::Data, - guild_uuid: Uuid, - name: String, - description: Option, - ) -> Result { - if !CHANNEL_REGEX.is_match(&name) { - return Err(Error::BadRequest("Channel name is invalid".to_string())) - } - - let mut conn = data.pool.get().await?; - - let channel_uuid = Uuid::now_v7(); - - let channels = Self::fetch_all(&data.pool, guild_uuid).await?; - - let channels_ordered = order_by_is_above(channels).await?; - - let last_channel = channels_ordered.last(); - - let new_channel = ChannelBuilder { - uuid: channel_uuid, - guild_uuid, - name: name.clone(), - description: description.clone(), - is_above: None, - }; - - insert_into(channels::table) - .values(new_channel.clone()) - .execute(&mut conn) - .await?; - - if let Some(old_last_channel) = last_channel { - use channels::dsl; - update(channels::table) - .filter(dsl::uuid.eq(old_last_channel.uuid)) - .set(dsl::is_above.eq(new_channel.uuid)) - .execute(&mut conn) - .await?; - } - - // returns different object because there's no reason to build the channelbuilder (wastes 1 database request) - let channel = Self { - uuid: channel_uuid, - guild_uuid, - name, - description, - is_above: None, - permissions: vec![], - }; - - data.set_cache_key(channel_uuid.to_string(), channel.clone(), 1800) - .await?; - - if data - .get_cache_key(format!("{}_channels", guild_uuid)) - .await - .is_ok() - { - data.del_cache_key(format!("{}_channels", guild_uuid)) - .await?; - } - - Ok(channel) - } - - pub async fn delete(self, data: &Data) -> Result<(), Error> { - let mut conn = data.pool.get().await?; - - use channels::dsl; - delete(channels::table) - .filter(dsl::uuid.eq(self.uuid)) - .execute(&mut conn) - .await?; - - if data.get_cache_key(self.uuid.to_string()).await.is_ok() { - data.del_cache_key(self.uuid.to_string()).await?; - } - - Ok(()) - } - - pub async fn fetch_messages( - &self, - data: &Data, - amount: i64, - offset: i64, - ) -> Result, Error> { - let mut conn = data.pool.get().await?; - - use messages::dsl; - let messages: Vec = load_or_empty( - dsl::messages - .filter(dsl::channel_uuid.eq(self.uuid)) - .select(MessageBuilder::as_select()) - .order(dsl::uuid.desc()) - .limit(amount) - .offset(offset) - .load(&mut conn) - .await, - )?; - - let message_futures = messages.iter().map(async move |b| b.build(data).await); - - futures::future::try_join_all(message_futures).await - } - - pub async fn new_message( - &self, - data: &Data, - user_uuid: Uuid, - message: String, - ) -> Result { - let message_uuid = Uuid::now_v7(); - - let message = MessageBuilder { - uuid: message_uuid, - channel_uuid: self.uuid, - user_uuid, - message, - }; - - let mut conn = data.pool.get().await?; - - insert_into(messages::table) - .values(message.clone()) - .execute(&mut conn) - .await?; - - message.build(data).await - } - - pub async fn set_name(&mut self, data: &Data, new_name: String) -> Result<(), Error> { - if !CHANNEL_REGEX.is_match(&new_name) { - return Err(Error::BadRequest("Channel name is invalid".to_string())) - } - - let mut conn = data.pool.get().await?; - - use channels::dsl; - update(channels::table) - .filter(dsl::uuid.eq(self.uuid)) - .set(dsl::name.eq(&new_name)) - .execute(&mut conn) - .await?; - - self.name = new_name; - - Ok(()) - } - - pub async fn set_description(&mut self, data: &Data, new_description: String) -> Result<(), Error> { - let mut conn = data.pool.get().await?; - - use channels::dsl; - update(channels::table) - .filter(dsl::uuid.eq(self.uuid)) - .set(dsl::description.eq(&new_description)) - .execute(&mut conn) - .await?; - - self.description = Some(new_description); - - Ok(()) - } - - pub async fn move_channel(&mut self, data: &Data, new_is_above: Uuid) -> Result<(), Error> { - let mut conn = data.pool.get().await?; - - use channels::dsl; - let old_above_uuid: Option = match dsl::channels - .filter(dsl::is_above.eq(self.uuid)) - .select(dsl::uuid) - .get_result(&mut conn) - .await - { - Ok(r) => Ok(Some(r)), - Err(e) if e == diesel::result::Error::NotFound => Ok(None), - Err(e) => Err(e), - }?; - - if let Some(uuid) = old_above_uuid { - update(channels::table) - .filter(dsl::uuid.eq(uuid)) - .set(dsl::is_above.eq(None::)) - .execute(&mut conn) - .await?; - } - - match update(channels::table) - .filter(dsl::is_above.eq(new_is_above)) - .set(dsl::is_above.eq(self.uuid)) - .execute(&mut conn) - .await - { - Ok(r) => Ok(r), - Err(e) if e == diesel::result::Error::NotFound => Ok(0), - Err(e) => Err(e), - }?; - - update(channels::table) - .filter(dsl::uuid.eq(self.uuid)) - .set(dsl::is_above.eq(new_is_above)) - .execute(&mut conn) - .await?; - - if let Some(uuid) = old_above_uuid { - update(channels::table) - .filter(dsl::uuid.eq(uuid)) - .set(dsl::is_above.eq(self.is_above)) - .execute(&mut conn) - .await?; - } - - self.is_above = Some(new_is_above); - - Ok(()) - } -} - -#[derive(Clone, Copy)] -pub enum Permissions { - SendMessage = 1, - CreateChannel = 2, - DeleteChannel = 4, - ManageChannel = 8, - CreateRole = 16, - DeleteRole = 32, - ManageRole = 64, - CreateInvite = 128, - ManageInvite = 256, - ManageServer = 512, - ManageMember = 1024, -} - -impl Permissions { - pub fn fetch_permissions(permissions: i64) -> Vec { - let all_perms = vec![ - Self::SendMessage, - Self::CreateChannel, - Self::DeleteChannel, - Self::ManageChannel, - Self::CreateRole, - Self::DeleteRole, - Self::ManageRole, - Self::CreateInvite, - Self::ManageInvite, - Self::ManageServer, - Self::ManageMember, - ]; - - all_perms - .into_iter() - .filter(|p| permissions & (*p as i64) != 0) - .collect() - } -} - -#[derive(Serialize, Queryable, Selectable, Insertable, Clone)] -#[diesel(table_name = guilds)] -#[diesel(check_for_backend(diesel::pg::Pg))] -struct GuildBuilder { - uuid: Uuid, - name: String, - description: Option, - icon: Option, - owner_uuid: Uuid, -} - -impl GuildBuilder { - async fn build(self, conn: &mut Conn) -> Result { - let member_count = Member::count(conn, self.uuid).await?; - - let roles = Role::fetch_all(conn, self.uuid).await?; - - Ok(Guild { - uuid: self.uuid, - name: self.name, - description: self.description, - icon: self.icon.and_then(|i| i.parse().ok()), - owner_uuid: self.owner_uuid, - roles, - member_count, - }) - } -} - -#[derive(Serialize)] -pub struct Guild { - pub uuid: Uuid, - name: String, - description: Option, - icon: Option, - owner_uuid: Uuid, - pub roles: Vec, - member_count: i64, -} - -impl Guild { - pub async fn fetch_one(conn: &mut Conn, guild_uuid: Uuid) -> Result { - use guilds::dsl; - let guild_builder: GuildBuilder = dsl::guilds - .filter(dsl::uuid.eq(guild_uuid)) - .select(GuildBuilder::as_select()) - .get_result(conn) - .await?; - - guild_builder.build(conn).await - } - - pub async fn fetch_amount( - pool: &deadpool::managed::Pool< - AsyncDieselConnectionManager, - Conn, - >, - offset: i64, - amount: i64, - ) -> Result, Error> { - // Fetch guild data from database - let mut conn = pool.get().await?; - - use guilds::dsl; - let guild_builders: Vec = load_or_empty( - dsl::guilds - .select(GuildBuilder::as_select()) - .order_by(dsl::uuid) - .offset(offset) - .limit(amount) - .load(&mut conn) - .await, - )?; - - // Process each guild concurrently - let guild_futures = guild_builders.iter().map(async move |g| { - let mut conn = pool.get().await?; - g.clone().build(&mut conn).await - }); - - // Execute all futures concurrently and collect results - futures::future::try_join_all(guild_futures).await - } - - pub async fn new(conn: &mut Conn, name: String, owner_uuid: Uuid) -> Result { - let guild_uuid = Uuid::now_v7(); - - let guild_builder = GuildBuilder { - uuid: guild_uuid, - name: name.clone(), - description: None, - icon: None, - owner_uuid, - }; - - insert_into(guilds::table) - .values(guild_builder) - .execute(conn) - .await?; - - let member_uuid = Uuid::now_v7(); - - let member = MemberBuilder { - uuid: member_uuid, - nickname: None, - user_uuid: owner_uuid, - guild_uuid, - }; - - insert_into(guild_members::table) - .values(member) - .execute(conn) - .await?; - - Ok(Guild { - uuid: guild_uuid, - name, - description: None, - icon: None, - owner_uuid, - roles: vec![], - member_count: 1, - }) - } - - pub async fn get_invites(&self, conn: &mut Conn) -> Result, Error> { - use invites::dsl; - let invites = load_or_empty( - dsl::invites - .filter(dsl::guild_uuid.eq(self.uuid)) - .select(Invite::as_select()) - .load(conn) - .await, - )?; - - Ok(invites) - } - - pub async fn create_invite( - &self, - conn: &mut Conn, - user_uuid: Uuid, - custom_id: Option, - ) -> Result { - let invite_id; - - if let Some(id) = custom_id { - invite_id = id; - if invite_id.len() > 32 { - return Err(Error::BadRequest("MAX LENGTH".to_string())); - } - } else { - let charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - - invite_id = random_string::generate(8, charset); - } - - let invite = Invite { - id: invite_id, - user_uuid, - guild_uuid: self.uuid, - }; - - insert_into(invites::table) - .values(invite.clone()) - .execute(conn) - .await?; - - Ok(invite) - } - - // FIXME: Horrible security - pub async fn set_icon( - &mut self, - bunny_cdn: &bunny_api_tokio::Client, - conn: &mut Conn, - cdn_url: Url, - icon: BytesMut, - ) -> Result<(), Error> { - let icon_clone = icon.clone(); - let image_type = task::spawn_blocking(move || image_check(icon_clone)).await??; - - if let Some(icon) = &self.icon { - let relative_url = icon.path().trim_start_matches('/'); - - bunny_cdn.storage.delete(relative_url).await?; - } - - let path = format!("icons/{}/icon.{}", self.uuid, image_type); - - bunny_cdn.storage.upload(path.clone(), icon.into()).await?; - - let icon_url = cdn_url.join(&path)?; - - use guilds::dsl; - update(guilds::table) - .filter(dsl::uuid.eq(self.uuid)) - .set(dsl::icon.eq(icon_url.as_str())) - .execute(conn) - .await?; - - self.icon = Some(icon_url); - - Ok(()) - } -} - -#[derive(Serialize, Clone, Queryable, Selectable, Insertable)] -#[diesel(table_name = roles)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct Role { - uuid: Uuid, - guild_uuid: Uuid, - name: String, - color: i32, - is_above: Option, - permissions: i64, -} - -impl HasUuid for Role { - fn uuid(&self) -> &Uuid { - self.uuid.as_ref() - } -} - -impl HasIsAbove for Role { - fn is_above(&self) -> Option<&Uuid> { - self.is_above.as_ref() - } -} - -impl Role { - pub async fn fetch_all(conn: &mut Conn, guild_uuid: Uuid) -> Result, Error> { - use roles::dsl; - let roles: Vec = load_or_empty( - dsl::roles - .filter(dsl::guild_uuid.eq(guild_uuid)) - .select(Role::as_select()) - .load(conn) - .await, - )?; - - Ok(roles) - } - - pub async fn fetch_one(conn: &mut Conn, role_uuid: Uuid) -> Result { - use roles::dsl; - let role: Role = dsl::roles - .filter(dsl::uuid.eq(role_uuid)) - .select(Role::as_select()) - .get_result(conn) - .await?; - - Ok(role) - } - - pub async fn new(conn: &mut Conn, guild_uuid: Uuid, name: String) -> Result { - let role_uuid = Uuid::now_v7(); - - let roles = Self::fetch_all(conn, guild_uuid).await?; - - let roles_ordered = order_by_is_above(roles).await?; - - let last_role = roles_ordered.last(); - - let new_role = Role { - uuid: role_uuid, - guild_uuid, - name, - color: 16777215, - is_above: None, - permissions: 0, - }; - - insert_into(roles::table) - .values(new_role.clone()) - .execute(conn) - .await?; - - if let Some(old_last_role) = last_role { - use roles::dsl; - update(roles::table) - .filter(dsl::uuid.eq(old_last_role.uuid)) - .set(dsl::is_above.eq(new_role.uuid)) - .execute(conn) - .await?; - } - - Ok(new_role) - } -} - -#[derive(Serialize, Queryable, Selectable, Insertable)] -#[diesel(table_name = guild_members)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct MemberBuilder { - pub uuid: Uuid, - pub nickname: Option, - pub user_uuid: Uuid, - pub guild_uuid: Uuid, -} - -impl MemberBuilder { - async fn build(&self, data: &Data) -> Result { - let user = User::fetch_one(data, self.user_uuid).await?; - - Ok(Member { - uuid: self.uuid, - nickname: self.nickname.clone(), - user_uuid: self.user_uuid, - guild_uuid: self.guild_uuid, - user, - }) - } -} - -#[derive(Serialize, Deserialize)] -pub struct Member { - pub uuid: Uuid, - pub nickname: Option, - pub user_uuid: Uuid, - pub guild_uuid: Uuid, - user: User, -} - -impl Member { - async fn count(conn: &mut Conn, guild_uuid: Uuid) -> Result { - use guild_members::dsl; - let count: i64 = dsl::guild_members - .filter(dsl::guild_uuid.eq(guild_uuid)) - .count() - .get_result(conn) - .await?; - - Ok(count) - } - - pub async fn check_membership( - conn: &mut Conn, - user_uuid: Uuid, - guild_uuid: Uuid, - ) -> Result<(), Error> { - use guild_members::dsl; - dsl::guild_members - .filter(dsl::user_uuid.eq(user_uuid)) - .filter(dsl::guild_uuid.eq(guild_uuid)) - .select(MemberBuilder::as_select()) - .get_result(conn) - .await?; - - Ok(()) - } - - pub async fn fetch_one(data: &Data, user_uuid: Uuid, guild_uuid: Uuid) -> Result { - let mut conn = data.pool.get().await?; - - use guild_members::dsl; - let member: MemberBuilder = dsl::guild_members - .filter(dsl::user_uuid.eq(user_uuid)) - .filter(dsl::guild_uuid.eq(guild_uuid)) - .select(MemberBuilder::as_select()) - .get_result(&mut conn) - .await?; - - member.build(data).await - } - - pub async fn fetch_all(data: &Data, guild_uuid: Uuid) -> Result, Error> { - let mut conn = data.pool.get().await?; - - use guild_members::dsl; - let member_builders: Vec = load_or_empty( - dsl::guild_members - .filter(dsl::guild_uuid.eq(guild_uuid)) - .select(MemberBuilder::as_select()) - .load(&mut conn) - .await, - )?; - - let member_futures = member_builders - .iter() - .map(async move |m| m.build(data).await); - - futures::future::try_join_all(member_futures).await - } - - pub async fn new(data: &Data, user_uuid: Uuid, guild_uuid: Uuid) -> Result { - let mut conn = data.pool.get().await?; - - let member_uuid = Uuid::now_v7(); - - let member = MemberBuilder { - uuid: member_uuid, - guild_uuid, - user_uuid, - nickname: None, - }; - - insert_into(guild_members::table) - .values(&member) - .execute(&mut conn) - .await?; - - member.build(data).await - } -} - -#[derive(Clone, Queryable, Selectable, Insertable)] -#[diesel(table_name = messages)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct MessageBuilder { - uuid: Uuid, - channel_uuid: Uuid, - user_uuid: Uuid, - message: String, -} - -impl MessageBuilder { - pub async fn build(&self, data: &Data) -> Result { - let user = User::fetch_one(data, self.user_uuid).await?; - - Ok(Message { - uuid: self.uuid, - channel_uuid: self.channel_uuid, - user_uuid: self.user_uuid, - message: self.message.clone(), - user, - }) - } -} - -#[derive(Clone, Serialize)] -pub struct Message { - uuid: Uuid, - channel_uuid: Uuid, - user_uuid: Uuid, - message: String, - user: User, -} - -/// Server invite struct -#[derive(Clone, Serialize, Queryable, Selectable, Insertable)] -pub struct Invite { - /// case-sensitive alphanumeric string with a fixed length of 8 characters, can be up to 32 characters for custom invites - id: String, - /// User that created the invite - user_uuid: Uuid, - /// UUID of the guild that the invite belongs to - pub guild_uuid: Uuid, -} - -impl Invite { - pub async fn fetch_one(conn: &mut Conn, invite_id: String) -> Result { - use invites::dsl; - let invite: Invite = dsl::invites - .filter(dsl::id.eq(invite_id)) - .select(Invite::as_select()) - .get_result(conn) - .await?; - - Ok(invite) - } -} - -#[derive(Deserialize, Serialize, Clone, Queryable, Selectable)] -#[diesel(table_name = users)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct User { - uuid: Uuid, - username: String, - display_name: Option, - avatar: Option, - pronouns: Option, - about: Option, -} - -impl User { - pub async fn fetch_one(data: &Data, user_uuid: Uuid) -> Result { - let mut conn = data.pool.get().await?; - - if let Ok(cache_hit) = data.get_cache_key(user_uuid.to_string()).await { - return Ok(serde_json::from_str(&cache_hit)?); - } - - use users::dsl; - let user: User = dsl::users - .filter(dsl::uuid.eq(user_uuid)) - .select(User::as_select()) - .get_result(&mut conn) - .await?; - - data.set_cache_key(user_uuid.to_string(), user.clone(), 1800) - .await?; - - Ok(user) - } - - pub async fn fetch_amount( - conn: &mut Conn, - offset: i64, - amount: i64, - ) -> Result, Error> { - use users::dsl; - let users: Vec = load_or_empty( - dsl::users - .limit(amount) - .offset(offset) - .select(User::as_select()) - .load(conn) - .await, - )?; - - Ok(users) - } -} - -#[derive(Serialize, Queryable, Selectable)] -#[diesel(table_name = users)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct Me { - pub uuid: Uuid, - username: String, - display_name: Option, - avatar: Option, - pronouns: Option, - about: Option, - email: String, - pub email_verified: bool, -} - -impl Me { - pub async fn get(conn: &mut Conn, user_uuid: Uuid) -> Result { - use users::dsl; - let me: Me = dsl::users - .filter(dsl::uuid.eq(user_uuid)) - .select(Me::as_select()) - .get_result(conn) - .await?; - - Ok(me) - } - - pub async fn fetch_memberships(&self, conn: &mut Conn) -> Result, Error> { - use guild_members::dsl; - let memberships: Vec = load_or_empty( - dsl::guild_members - .filter(dsl::user_uuid.eq(self.uuid)) - .select(MemberBuilder::as_select()) - .load(conn) - .await - )?; - - let mut guilds: Vec = vec![]; - - for membership in memberships { - use guilds::dsl; - guilds.push( - dsl::guilds - .filter(dsl::uuid.eq(membership.guild_uuid)) - .select(GuildBuilder::as_select()) - .get_result(conn) - .await? - .build(conn) - .await?, - ) - } - - Ok(guilds) - } - - pub async fn set_avatar( - &mut self, - data: &Data, - cdn_url: Url, - avatar: BytesMut, - ) -> Result<(), Error> { - let avatar_clone = avatar.clone(); - let image_type = task::spawn_blocking(move || image_check(avatar_clone)).await??; - - let mut conn = data.pool.get().await?; - - if let Some(avatar) = &self.avatar { - let avatar_url: Url = avatar.parse()?; - - let relative_url = avatar_url.path().trim_start_matches('/'); - - data.bunny_cdn.storage.delete(relative_url).await?; - } - - let path = format!("avatar/{}/avatar.{}", self.uuid, image_type); - - data. - bunny_cdn - .storage - .upload(path.clone(), avatar.into()) - .await?; - - let avatar_url = cdn_url.join(&path)?; - - use users::dsl; - update(users::table) - .filter(dsl::uuid.eq(self.uuid)) - .set(dsl::avatar.eq(avatar_url.as_str())) - .execute(&mut conn) - .await?; - - if data.get_cache_key(self.uuid.to_string()).await.is_ok() { - data.del_cache_key(self.uuid.to_string()).await? - } - - self.avatar = Some(avatar_url.to_string()); - - Ok(()) - } - - pub async fn verify_email(&self, conn: &mut Conn) -> Result<(), Error> { - use users::dsl; - update(users::table) - .filter(dsl::uuid.eq(self.uuid)) - .set(dsl::email_verified.eq(true)) - .execute(conn) - .await?; - - Ok(()) - } - - pub async fn set_username( - &mut self, - data: &Data, - new_username: String, - ) -> Result<(), Error> { - if !USERNAME_REGEX.is_match(&new_username) { - return Err(Error::BadRequest("Invalid username".to_string())); - } - - let mut conn = data.pool.get().await?; - - use users::dsl; - update(users::table) - .filter(dsl::uuid.eq(self.uuid)) - .set(dsl::username.eq(new_username.as_str())) - .execute(&mut conn) - .await?; - - if data.get_cache_key(self.uuid.to_string()).await.is_ok() { - data.del_cache_key(self.uuid.to_string()).await? - } - - self.username = new_username; - - Ok(()) - } - - pub async fn set_display_name( - &mut self, - data: &Data, - new_display_name: String, - ) -> Result<(), Error> { - let mut conn = data.pool.get().await?; - - use users::dsl; - update(users::table) - .filter(dsl::uuid.eq(self.uuid)) - .set(dsl::display_name.eq(new_display_name.as_str())) - .execute(&mut conn) - .await?; - - if data.get_cache_key(self.uuid.to_string()).await.is_ok() { - data.del_cache_key(self.uuid.to_string()).await? - } - - self.display_name = Some(new_display_name); - - Ok(()) - } - - pub async fn set_email(&mut self, data: &Data, new_email: String) -> Result<(), Error> { - if !EMAIL_REGEX.is_match(&new_email) { - return Err(Error::BadRequest("Invalid username".to_string())); - } - - let mut conn = data.pool.get().await?; - - use users::dsl; - update(users::table) - .filter(dsl::uuid.eq(self.uuid)) - .set(( - dsl::email.eq(new_email.as_str()), - dsl::email_verified.eq(false), - )) - .execute(&mut conn) - .await?; - - if data.get_cache_key(self.uuid.to_string()).await.is_ok() { - data.del_cache_key(self.uuid.to_string()).await? - } - - self.email = new_email; - - Ok(()) - } - - pub async fn set_pronouns(&mut self, data: &Data, new_pronouns: String) -> Result<(), Error> { - let mut conn = data.pool.get().await?; - - use users::dsl; - update(users::table) - .filter(dsl::uuid.eq(self.uuid)) - .set(( - dsl::pronouns.eq(new_pronouns.as_str()), - )) - .execute(&mut conn) - .await?; - - if data.get_cache_key(self.uuid.to_string()).await.is_ok() { - data.del_cache_key(self.uuid.to_string()).await? - } - - Ok(()) - } - - pub async fn set_about(&mut self, data: &Data, new_about: String) -> Result<(), Error> { - let mut conn = data.pool.get().await?; - - use users::dsl; - update(users::table) - .filter(dsl::uuid.eq(self.uuid)) - .set(( - dsl::about.eq(new_about.as_str()), - )) - .execute(&mut conn) - .await?; - - if data.get_cache_key(self.uuid.to_string()).await.is_ok() { - data.del_cache_key(self.uuid.to_string()).await? - } - - Ok(()) - } -} - -#[derive(Deserialize)] -pub struct StartAmountQuery { - pub start: Option, - pub amount: Option, -} - -#[derive(Selectable, Queryable)] -#[diesel(table_name = email_tokens)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct EmailToken { - user_uuid: Uuid, - pub token: String, - pub created_at: chrono::DateTime, -} - -impl EmailToken { - pub async fn get(conn: &mut Conn, user_uuid: Uuid) -> Result { - use email_tokens::dsl; - let email_token = dsl::email_tokens - .filter(dsl::user_uuid.eq(user_uuid)) - .select(EmailToken::as_select()) - .get_result(conn) - .await?; - - Ok(email_token) - } - - #[allow(clippy::new_ret_no_self)] - pub async fn new(data: &Data, me: Me) -> Result<(), Error> { - let token = generate_refresh_token()?; - - let mut conn = data.pool.get().await?; - - use email_tokens::dsl; - insert_into(email_tokens::table) - .values(( - dsl::user_uuid.eq(me.uuid), - dsl::token.eq(&token), - dsl::created_at.eq(now), - )) - .execute(&mut conn) - .await?; - - let mut verify_endpoint = data.config.web.frontend_url.join("verify-email")?; - - verify_endpoint.set_query(Some(&format!("token={}", token))); - - let email = data - .mail_client - .message_builder() - .to(me.email.parse()?) - .subject(format!("{} E-mail Verification", data.config.instance.name)) - .multipart(MultiPart::alternative_plain_html( - format!("Verify your {} account\n\nHello, {}!\nThanks for creating a new account on Gorb.\nThe final step to create your account is to verify your email address by visiting the page, within 24 hours.\n\n{}\n\nIf you didn't ask to verify this address, you can safely ignore this email\n\nThanks, The gorb team.", data.config.instance.name, me.username, verify_endpoint), - format!(r#"

Verify your {} Account

Hello, {}!

Thanks for creating a new account on Gorb.

The final step to create your account is to verify your email address by clicking the button below, within 24 hours.

VERIFY ACCOUNT

If you didn't ask to verify this address, you can safely ignore this email.

"#, data.config.instance.name, me.username, verify_endpoint) - ))?; - - data.mail_client.send_mail(email).await?; - - Ok(()) - } - - pub async fn delete(&self, conn: &mut Conn) -> Result<(), Error> { - use email_tokens::dsl; - delete(email_tokens::table) - .filter(dsl::user_uuid.eq(self.user_uuid)) - .filter(dsl::token.eq(&self.token)) - .execute(conn) - .await?; - - Ok(()) - } -} - -#[derive(Selectable, Queryable)] -#[diesel(table_name = password_reset_tokens)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct PasswordResetToken { - user_uuid: Uuid, - pub token: String, - pub created_at: chrono::DateTime, -} - -impl PasswordResetToken { - pub async fn get(conn: &mut Conn, token: String) -> Result { - use password_reset_tokens::dsl; - let password_reset_token = dsl::password_reset_tokens - .filter(dsl::token.eq(token)) - .select(PasswordResetToken::as_select()) - .get_result(conn) - .await?; - - Ok(password_reset_token) - } - - pub async fn get_with_identifier( - conn: &mut Conn, - identifier: String, - ) -> Result { - let user_uuid = user_uuid_from_identifier(conn, &identifier).await?; - - use password_reset_tokens::dsl; - let password_reset_token = dsl::password_reset_tokens - .filter(dsl::user_uuid.eq(user_uuid)) - .select(PasswordResetToken::as_select()) - .get_result(conn) - .await?; - - Ok(password_reset_token) - } - - #[allow(clippy::new_ret_no_self)] - pub async fn new(data: &Data, identifier: String) -> Result<(), Error> { - let token = generate_refresh_token()?; - - let mut conn = data.pool.get().await?; - - let user_uuid = user_uuid_from_identifier(&mut conn, &identifier).await?; - - global_checks(data, user_uuid).await?; - - use users::dsl as udsl; - let (username, email_address): (String, String) = udsl::users - .filter(udsl::uuid.eq(user_uuid)) - .select((udsl::username, udsl::email)) - .get_result(&mut conn) - .await?; - - use password_reset_tokens::dsl; - insert_into(password_reset_tokens::table) - .values(( - dsl::user_uuid.eq(user_uuid), - dsl::token.eq(&token), - dsl::created_at.eq(now), - )) - .execute(&mut conn) - .await?; - - let mut reset_endpoint = data.config.web.frontend_url.join("reset-password")?; - - reset_endpoint.set_query(Some(&format!("token={}", token))); - - let email = data - .mail_client - .message_builder() - .to(email_address.parse()?) - .subject(format!("{} Password Reset", data.config.instance.name)) - .multipart(MultiPart::alternative_plain_html( - format!("{} Password Reset\n\nHello, {}!\nSomeone requested a password reset for your Gorb account.\nClick the button below within 24 hours to reset your password.\n\n{}\n\nIf you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.\n\nThanks, The gorb team.", data.config.instance.name, username, reset_endpoint), - format!(r#"

{} Password Reset

Hello, {}!

Someone requested a password reset for your Gorb account.

Click the button below within 24 hours to reset your password.

RESET PASSWORD

If you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.

"#, data.config.instance.name, username, reset_endpoint) - ))?; - - data.mail_client.send_mail(email).await?; - - Ok(()) - } - - pub async fn set_password(&self, data: &Data, password: String) -> Result<(), Error> { - if !PASSWORD_REGEX.is_match(&password) { - return Err(Error::BadRequest( - "Please provide a valid password".to_string(), - )); - } - - let salt = SaltString::generate(&mut OsRng); - - let hashed_password = data - .argon2 - .hash_password(password.as_bytes(), &salt) - .map_err(|e| Error::PasswordHashError(e.to_string()))?; - - let mut conn = data.pool.get().await?; - - use users::dsl; - update(users::table) - .filter(dsl::uuid.eq(self.user_uuid)) - .set(dsl::password.eq(hashed_password.to_string())) - .execute(&mut conn) - .await?; - - let (username, email_address): (String, String) = dsl::users - .filter(dsl::uuid.eq(self.user_uuid)) - .select((dsl::username, dsl::email)) - .get_result(&mut conn) - .await?; - - let login_page = data.config.web.frontend_url.join("login")?; - - let email = data - .mail_client - .message_builder() - .to(email_address.parse()?) - .subject(format!("Your {} Password has been Reset", data.config.instance.name)) - .multipart(MultiPart::alternative_plain_html( - format!("{} Password Reset Confirmation\n\nHello, {}!\nYour password has been successfully reset for your Gorb account.\nIf you did not initiate this change, please click the link below to reset your password immediately.\n\n{}\n\nThanks, The gorb team.", data.config.instance.name, username, login_page), - format!(r#"

{} Password Reset Confirmation

Hello, {}!

Your password has been successfully reset for your Gorb account.

If you did not initiate this change, please click the button below to reset your password immediately.

RESET PASSWORD
"#, data.config.instance.name, username, login_page) - ))?; - - data.mail_client.send_mail(email).await?; - - self.delete(&mut conn).await - } - - pub async fn delete(&self, conn: &mut Conn) -> Result<(), Error> { - use password_reset_tokens::dsl; - delete(password_reset_tokens::table) - .filter(dsl::user_uuid.eq(self.user_uuid)) - .filter(dsl::token.eq(&self.token)) - .execute(conn) - .await?; - - Ok(()) - } -} diff --git a/src/utils.rs b/src/utils.rs index 3bf7332..3172cec 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -19,8 +19,8 @@ use crate::{ Conn, Data, config::Config, error::Error, + objects::{HasIsAbove, HasUuid}, schema::users, - structs::{HasIsAbove, HasUuid}, }; pub static EMAIL_REGEX: LazyLock = LazyLock::new(|| { From c01570707de0277397b7e99cf2ff193a68742d54 Mon Sep 17 00:00:00 2001 From: Radical Date: Mon, 2 Jun 2025 00:30:10 +0200 Subject: [PATCH 056/160] style: cargo clippy --- src/main.rs | 2 +- src/objects/channel.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 9a24d3d..540a237 100644 --- a/src/main.rs +++ b/src/main.rs @@ -155,7 +155,7 @@ async fn main() -> Result<(), Error> { App::new() .app_data(web::Data::new(data.clone())) .wrap(cors) - .service(api::web(&data.config.web.backend_url.path())) + .service(api::web(data.config.web.backend_url.path())) }) .bind((web.ip, web.port))? .run() diff --git a/src/objects/channel.rs b/src/objects/channel.rs index c9b5f1f..9b756f2 100644 --- a/src/objects/channel.rs +++ b/src/objects/channel.rs @@ -309,7 +309,7 @@ impl Channel { .await { Ok(r) => Ok(Some(r)), - Err(e) if e == diesel::result::Error::NotFound => Ok(None), + Err(diesel::result::Error::NotFound) => Ok(None), Err(e) => Err(e), }?; @@ -328,7 +328,7 @@ impl Channel { .await { Ok(r) => Ok(r), - Err(e) if e == diesel::result::Error::NotFound => Ok(0), + Err(diesel::result::Error::NotFound) => Ok(0), Err(e) => Err(e), }?; From 4cbe551061d5a0feb82eb4a993629cbd707adcc0 Mon Sep 17 00:00:00 2001 From: Radical Date: Mon, 2 Jun 2025 17:50:11 +0200 Subject: [PATCH 057/160] fix: make custom id optional --- src/api/v1/guilds/uuid/invites/mod.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/api/v1/guilds/uuid/invites/mod.rs b/src/api/v1/guilds/uuid/invites/mod.rs index f4f06bc..bb8269c 100644 --- a/src/api/v1/guilds/uuid/invites/mod.rs +++ b/src/api/v1/guilds/uuid/invites/mod.rs @@ -12,7 +12,7 @@ use crate::{ #[derive(Deserialize)] struct InviteRequest { - custom_id: String, + custom_id: Option, } #[get("{uuid}/invites")] @@ -46,7 +46,7 @@ pub async fn get( pub async fn create( req: HttpRequest, path: web::Path<(Uuid,)>, - invite_request: web::Json>, + invite_request: web::Json, data: web::Data, ) -> Result { let headers = req.headers(); @@ -65,9 +65,7 @@ pub async fn create( let guild = Guild::fetch_one(&mut conn, guild_uuid).await?; - let custom_id = invite_request.as_ref().map(|ir| ir.custom_id.clone()); - - let invite = guild.create_invite(&mut conn, uuid, custom_id).await?; + let invite = guild.create_invite(&mut conn, uuid, invite_request.custom_id.clone()).await?; Ok(HttpResponse::Ok().json(invite)) } From b223dff4ba6734c692236cdd3d029590859704c7 Mon Sep 17 00:00:00 2001 From: Radical Date: Tue, 3 Jun 2025 11:01:33 +0000 Subject: [PATCH 058/160] feat: move email tokens to valkey No need to have them in permanent DB storage when they are temporary --- .../down.sql | 7 +++ .../up.sql | 2 + src/api/v1/auth/verify_email.rs | 13 ++--- src/objects/email_token.rs | 47 ++++++------------- src/schema.rs | 11 ----- 5 files changed, 27 insertions(+), 53 deletions(-) create mode 100644 migrations/2025-06-03-103311_remove_email_tokens/down.sql create mode 100644 migrations/2025-06-03-103311_remove_email_tokens/up.sql diff --git a/migrations/2025-06-03-103311_remove_email_tokens/down.sql b/migrations/2025-06-03-103311_remove_email_tokens/down.sql new file mode 100644 index 0000000..e8f0350 --- /dev/null +++ b/migrations/2025-06-03-103311_remove_email_tokens/down.sql @@ -0,0 +1,7 @@ +-- This file should undo anything in `up.sql` +CREATE TABLE email_tokens ( + token VARCHAR(64) NOT NULL, + user_uuid uuid UNIQUE NOT NULL REFERENCES users(uuid), + created_at TIMESTAMPTZ NOT NULL, + PRIMARY KEY (token, user_uuid) +); diff --git a/migrations/2025-06-03-103311_remove_email_tokens/up.sql b/migrations/2025-06-03-103311_remove_email_tokens/up.sql new file mode 100644 index 0000000..b41afe5 --- /dev/null +++ b/migrations/2025-06-03-103311_remove_email_tokens/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +DROP TABLE email_tokens; diff --git a/src/api/v1/auth/verify_email.rs b/src/api/v1/auth/verify_email.rs index 0f23649..e596500 100644 --- a/src/api/v1/auth/verify_email.rs +++ b/src/api/v1/auth/verify_email.rs @@ -46,20 +46,15 @@ pub async fn get( let me = Me::get(&mut conn, uuid).await?; - let email_token = EmailToken::get(&mut conn, me.uuid).await?; + let email_token = EmailToken::get(&data, me.uuid).await?; if query.token != email_token.token { return Ok(HttpResponse::Unauthorized().finish()); } - if Utc::now().signed_duration_since(email_token.created_at) > Duration::hours(24) { - email_token.delete(&mut conn).await?; - return Ok(HttpResponse::Gone().finish()); - } - me.verify_email(&mut conn).await?; - email_token.delete(&mut conn).await?; + email_token.delete(&data).await?; Ok(HttpResponse::Ok().finish()) } @@ -90,9 +85,9 @@ pub async fn post(req: HttpRequest, data: web::Data) -> Result Duration::hours(1) { - email_token.delete(&mut conn).await?; + email_token.delete(&data).await?; } else { return Err(Error::TooManyRequests( "Please allow 1 hour before sending a new email".to_string(), diff --git a/src/objects/email_token.rs b/src/objects/email_token.rs index e458cf7..f55de8c 100644 --- a/src/objects/email_token.rs +++ b/src/objects/email_token.rs @@ -1,19 +1,13 @@ use chrono::Utc; -use diesel::{ - ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper, delete, dsl::now, - insert_into, -}; -use diesel_async::RunQueryDsl; use lettre::message::MultiPart; +use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::{Conn, Data, error::Error, schema::email_tokens, utils::generate_refresh_token}; +use crate::{Data, error::Error, utils::generate_refresh_token}; use super::Me; -#[derive(Selectable, Queryable)] -#[diesel(table_name = email_tokens)] -#[diesel(check_for_backend(diesel::pg::Pg))] +#[derive(Serialize, Deserialize)] pub struct EmailToken { user_uuid: Uuid, pub token: String, @@ -21,13 +15,8 @@ pub struct EmailToken { } impl EmailToken { - pub async fn get(conn: &mut Conn, user_uuid: Uuid) -> Result { - use email_tokens::dsl; - let email_token = dsl::email_tokens - .filter(dsl::user_uuid.eq(user_uuid)) - .select(EmailToken::as_select()) - .get_result(conn) - .await?; + pub async fn get(data: &Data, user_uuid: Uuid) -> Result { + let email_token = serde_json::from_str(&data.get_cache_key(format!("{}_email_verify", user_uuid)).await?)?; Ok(email_token) } @@ -36,17 +25,14 @@ impl EmailToken { pub async fn new(data: &Data, me: Me) -> Result<(), Error> { let token = generate_refresh_token()?; - let mut conn = data.pool.get().await?; + let email_token = EmailToken { + user_uuid: me.uuid, + token: token.clone(), + // TODO: Check if this can be replaced with something built into valkey + created_at: Utc::now() + }; - use email_tokens::dsl; - insert_into(email_tokens::table) - .values(( - dsl::user_uuid.eq(me.uuid), - dsl::token.eq(&token), - dsl::created_at.eq(now), - )) - .execute(&mut conn) - .await?; + data.set_cache_key(format!("{}_email_verify", me.uuid), email_token, 86400).await?; let mut verify_endpoint = data.config.web.frontend_url.join("verify-email")?; @@ -67,13 +53,8 @@ impl EmailToken { Ok(()) } - pub async fn delete(&self, conn: &mut Conn) -> Result<(), Error> { - use email_tokens::dsl; - delete(email_tokens::table) - .filter(dsl::user_uuid.eq(self.user_uuid)) - .filter(dsl::token.eq(&self.token)) - .execute(conn) - .await?; + pub async fn delete(&self, data: &Data) -> Result<(), Error> { + data.del_cache_key(format!("{}_email_verify", self.user_uuid)).await?; Ok(()) } diff --git a/src/schema.rs b/src/schema.rs index 09ea7a3..09fa08a 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -31,15 +31,6 @@ diesel::table! { } } -diesel::table! { - email_tokens (token, user_uuid) { - #[max_length = 64] - token -> Varchar, - user_uuid -> Uuid, - created_at -> Timestamptz, - } -} - diesel::table! { guild_members (uuid) { uuid -> Uuid, @@ -155,7 +146,6 @@ diesel::joinable!(access_tokens -> refresh_tokens (refresh_token)); diesel::joinable!(access_tokens -> users (uuid)); diesel::joinable!(channel_permissions -> channels (channel_uuid)); diesel::joinable!(channels -> guilds (guild_uuid)); -diesel::joinable!(email_tokens -> users (user_uuid)); diesel::joinable!(guild_members -> guilds (guild_uuid)); diesel::joinable!(guild_members -> users (user_uuid)); diesel::joinable!(guilds -> users (owner_uuid)); @@ -173,7 +163,6 @@ diesel::allow_tables_to_appear_in_same_query!( access_tokens, channel_permissions, channels, - email_tokens, guild_members, guilds, instance_permissions, From 419f37b108075eab5c67ad03c51bbfd8dc7e4a42 Mon Sep 17 00:00:00 2001 From: Radical Date: Tue, 3 Jun 2025 11:03:52 +0000 Subject: [PATCH 059/160] feat: move password reset tokens to valkey Also just as useless to keep in DB --- .../down.sql | 7 ++ .../up.sql | 2 + src/api/v1/auth/reset_password.rs | 15 +---- src/objects/password_reset_token.rs | 64 ++++++++----------- src/schema.rs | 11 ---- 5 files changed, 37 insertions(+), 62 deletions(-) create mode 100644 migrations/2025-06-03-110142_remove_password_reset_tokens/down.sql create mode 100644 migrations/2025-06-03-110142_remove_password_reset_tokens/up.sql diff --git a/migrations/2025-06-03-110142_remove_password_reset_tokens/down.sql b/migrations/2025-06-03-110142_remove_password_reset_tokens/down.sql new file mode 100644 index 0000000..009d9e4 --- /dev/null +++ b/migrations/2025-06-03-110142_remove_password_reset_tokens/down.sql @@ -0,0 +1,7 @@ +-- This file should undo anything in `up.sql` +CREATE TABLE password_reset_tokens ( + token VARCHAR(64) NOT NULL, + user_uuid uuid UNIQUE NOT NULL REFERENCES users(uuid), + created_at TIMESTAMPTZ NOT NULL, + PRIMARY KEY (token, user_uuid) +); diff --git a/migrations/2025-06-03-110142_remove_password_reset_tokens/up.sql b/migrations/2025-06-03-110142_remove_password_reset_tokens/up.sql new file mode 100644 index 0000000..181d7c5 --- /dev/null +++ b/migrations/2025-06-03-110142_remove_password_reset_tokens/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +DROP TABLE password_reset_tokens; diff --git a/src/api/v1/auth/reset_password.rs b/src/api/v1/auth/reset_password.rs index 4373a82..444266c 100644 --- a/src/api/v1/auth/reset_password.rs +++ b/src/api/v1/auth/reset_password.rs @@ -26,13 +26,11 @@ struct Query { /// #[get("/reset-password")] pub async fn get(query: web::Query, data: web::Data) -> Result { - let mut conn = data.pool.get().await?; - if let Ok(password_reset_token) = - PasswordResetToken::get_with_identifier(&mut conn, query.identifier.clone()).await + PasswordResetToken::get_with_identifier(&data, query.identifier.clone()).await { if Utc::now().signed_duration_since(password_reset_token.created_at) > Duration::hours(1) { - password_reset_token.delete(&mut conn).await?; + password_reset_token.delete(&data).await?; } else { return Err(Error::TooManyRequests( "Please allow 1 hour before sending a new email".to_string(), @@ -74,15 +72,8 @@ pub async fn post( reset_password: web::Json, data: web::Data, ) -> Result { - let mut conn = data.pool.get().await?; - let password_reset_token = - PasswordResetToken::get(&mut conn, reset_password.token.clone()).await?; - - if Utc::now().signed_duration_since(password_reset_token.created_at) > Duration::hours(24) { - password_reset_token.delete(&mut conn).await?; - return Ok(HttpResponse::Gone().finish()); - } + PasswordResetToken::get(&data, reset_password.token.clone()).await?; password_reset_token .set_password(&data, reset_password.password.clone()) diff --git a/src/objects/password_reset_token.rs b/src/objects/password_reset_token.rs index e3c7bca..0376d88 100644 --- a/src/objects/password_reset_token.rs +++ b/src/objects/password_reset_token.rs @@ -4,23 +4,21 @@ use argon2::{ }; use chrono::Utc; use diesel::{ - ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper, delete, dsl::now, - insert_into, update, + ExpressionMethods, QueryDsl, update, }; use diesel_async::RunQueryDsl; use lettre::message::MultiPart; +use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ - Conn, Data, + Data, error::Error, - schema::{password_reset_tokens, users}, + schema::users, utils::{PASSWORD_REGEX, generate_refresh_token, global_checks, user_uuid_from_identifier}, }; -#[derive(Selectable, Queryable)] -#[diesel(table_name = password_reset_tokens)] -#[diesel(check_for_backend(diesel::pg::Pg))] +#[derive(Serialize, Deserialize)] pub struct PasswordResetToken { user_uuid: Uuid, pub token: String, @@ -28,29 +26,22 @@ pub struct PasswordResetToken { } impl PasswordResetToken { - pub async fn get(conn: &mut Conn, token: String) -> Result { - use password_reset_tokens::dsl; - let password_reset_token = dsl::password_reset_tokens - .filter(dsl::token.eq(token)) - .select(PasswordResetToken::as_select()) - .get_result(conn) - .await?; + pub async fn get(data: &Data, token: String) -> Result { + let user_uuid: Uuid = serde_json::from_str(&data.get_cache_key(format!("{}", token)).await?)?; + let password_reset_token = serde_json::from_str(&data.get_cache_key(format!("{}_password_reset", user_uuid)).await?)?; Ok(password_reset_token) } pub async fn get_with_identifier( - conn: &mut Conn, + data: &Data, identifier: String, ) -> Result { - let user_uuid = user_uuid_from_identifier(conn, &identifier).await?; + let mut conn = data.pool.get().await?; - use password_reset_tokens::dsl; - let password_reset_token = dsl::password_reset_tokens - .filter(dsl::user_uuid.eq(user_uuid)) - .select(PasswordResetToken::as_select()) - .get_result(conn) - .await?; + let user_uuid = user_uuid_from_identifier(&mut conn, &identifier).await?; + + let password_reset_token = serde_json::from_str(&data.get_cache_key(format!("{}_password_reset", user_uuid)).await?)?; Ok(password_reset_token) } @@ -72,15 +63,14 @@ impl PasswordResetToken { .get_result(&mut conn) .await?; - use password_reset_tokens::dsl; - insert_into(password_reset_tokens::table) - .values(( - dsl::user_uuid.eq(user_uuid), - dsl::token.eq(&token), - dsl::created_at.eq(now), - )) - .execute(&mut conn) - .await?; + let password_reset_token = PasswordResetToken { + user_uuid, + token: token.clone(), + created_at: Utc::now(), + }; + + data.set_cache_key(format!("{}_password_reset", user_uuid), password_reset_token, 86400).await?; + data.set_cache_key(token.clone(), user_uuid, 86400).await?; let mut reset_endpoint = data.config.web.frontend_url.join("reset-password")?; @@ -144,16 +134,12 @@ impl PasswordResetToken { data.mail_client.send_mail(email).await?; - self.delete(&mut conn).await + self.delete(&data).await } - pub async fn delete(&self, conn: &mut Conn) -> Result<(), Error> { - use password_reset_tokens::dsl; - delete(password_reset_tokens::table) - .filter(dsl::user_uuid.eq(self.user_uuid)) - .filter(dsl::token.eq(&self.token)) - .execute(conn) - .await?; + pub async fn delete(&self, data: &Data) -> Result<(), Error> { + data.del_cache_key(format!("{}_password_reset", &self.user_uuid)).await?; + data.del_cache_key(format!("{}", &self.token)).await?; Ok(()) } diff --git a/src/schema.rs b/src/schema.rs index 09fa08a..aaef9c1 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -80,15 +80,6 @@ diesel::table! { } } -diesel::table! { - password_reset_tokens (token, user_uuid) { - #[max_length = 64] - token -> Varchar, - user_uuid -> Uuid, - created_at -> Timestamptz, - } -} - diesel::table! { refresh_tokens (token) { #[max_length = 64] @@ -154,7 +145,6 @@ diesel::joinable!(invites -> guilds (guild_uuid)); diesel::joinable!(invites -> users (user_uuid)); diesel::joinable!(messages -> channels (channel_uuid)); diesel::joinable!(messages -> users (user_uuid)); -diesel::joinable!(password_reset_tokens -> users (user_uuid)); diesel::joinable!(refresh_tokens -> users (uuid)); diesel::joinable!(role_members -> guild_members (member_uuid)); diesel::joinable!(roles -> guilds (guild_uuid)); @@ -168,7 +158,6 @@ diesel::allow_tables_to_appear_in_same_query!( instance_permissions, invites, messages, - password_reset_tokens, refresh_tokens, role_members, roles, From 05885418760e4bd786e9b25611edd686c183f1a8 Mon Sep 17 00:00:00 2001 From: Radical Date: Fri, 6 Jun 2025 17:20:02 +0200 Subject: [PATCH 060/160] feat: move ownership to member column instead of table column --- .../down.sql | 14 ++++++++++++++ .../up.sql | 14 ++++++++++++++ src/objects/guild.rs | 6 +----- src/objects/member.rs | 4 ++++ src/schema.rs | 3 +-- 5 files changed, 34 insertions(+), 7 deletions(-) create mode 100644 migrations/2025-06-06-145916_guild_ownership_changes/down.sql create mode 100644 migrations/2025-06-06-145916_guild_ownership_changes/up.sql diff --git a/migrations/2025-06-06-145916_guild_ownership_changes/down.sql b/migrations/2025-06-06-145916_guild_ownership_changes/down.sql new file mode 100644 index 0000000..21a08c9 --- /dev/null +++ b/migrations/2025-06-06-145916_guild_ownership_changes/down.sql @@ -0,0 +1,14 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE guilds +ADD COLUMN owner_uuid UUID REFERENCES users(uuid); + +UPDATE guilds g +SET owner_uuid = gm.user_uuid +FROM guild_members gm +WHERE gm.guild_uuid = g.uuid AND gm.is_owner = TRUE; + +ALTER TABLE guilds +ALTER COLUMN owner_uuid SET NOT NULL; + +ALTER TABLE guild_members +DROP COLUMN is_owner; diff --git a/migrations/2025-06-06-145916_guild_ownership_changes/up.sql b/migrations/2025-06-06-145916_guild_ownership_changes/up.sql new file mode 100644 index 0000000..b94323f --- /dev/null +++ b/migrations/2025-06-06-145916_guild_ownership_changes/up.sql @@ -0,0 +1,14 @@ +-- Your SQL goes here +ALTER TABLE guild_members +ADD COLUMN is_owner BOOLEAN NOT NULL DEFAULT false; + +UPDATE guild_members gm +SET is_owner = true +FROM guilds g +WHERE gm.guild_uuid = g.uuid AND gm.user_uuid = g.owner_uuid; + +CREATE UNIQUE INDEX one_owner_per_guild ON guild_members (guild_uuid) +WHERE is_owner; + +ALTER TABLE guilds +DROP COLUMN owner_uuid; diff --git a/src/objects/guild.rs b/src/objects/guild.rs index f5e973d..7d55595 100644 --- a/src/objects/guild.rs +++ b/src/objects/guild.rs @@ -26,7 +26,6 @@ pub struct GuildBuilder { name: String, description: Option, icon: Option, - owner_uuid: Uuid, } impl GuildBuilder { @@ -40,7 +39,6 @@ impl GuildBuilder { name: self.name, description: self.description, icon: self.icon.and_then(|i| i.parse().ok()), - owner_uuid: self.owner_uuid, roles, member_count, }) @@ -53,7 +51,6 @@ pub struct Guild { name: String, description: Option, icon: Option, - owner_uuid: Uuid, pub roles: Vec, member_count: i64, } @@ -110,7 +107,6 @@ impl Guild { name: name.clone(), description: None, icon: None, - owner_uuid, }; insert_into(guilds::table) @@ -125,6 +121,7 @@ impl Guild { nickname: None, user_uuid: owner_uuid, guild_uuid, + is_owner: true, }; insert_into(guild_members::table) @@ -137,7 +134,6 @@ impl Guild { name, description: None, icon: None, - owner_uuid, roles: vec![], member_count: 1, }) diff --git a/src/objects/member.rs b/src/objects/member.rs index f18e726..67312e4 100644 --- a/src/objects/member.rs +++ b/src/objects/member.rs @@ -17,6 +17,7 @@ pub struct MemberBuilder { pub nickname: Option, pub user_uuid: Uuid, pub guild_uuid: Uuid, + pub is_owner: bool, } impl MemberBuilder { @@ -28,6 +29,7 @@ impl MemberBuilder { nickname: self.nickname.clone(), user_uuid: self.user_uuid, guild_uuid: self.guild_uuid, + is_owner: self.is_owner, user, }) } @@ -39,6 +41,7 @@ pub struct Member { pub nickname: Option, pub user_uuid: Uuid, pub guild_uuid: Uuid, + pub is_owner: bool, user: User, } @@ -113,6 +116,7 @@ impl Member { guild_uuid, user_uuid, nickname: None, + is_owner: false, }; insert_into(guild_members::table) diff --git a/src/schema.rs b/src/schema.rs index aaef9c1..c7a350c 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -38,13 +38,13 @@ diesel::table! { user_uuid -> Uuid, #[max_length = 100] nickname -> Nullable, + is_owner -> Bool, } } diesel::table! { guilds (uuid) { uuid -> Uuid, - owner_uuid -> Uuid, #[max_length = 100] name -> Varchar, #[max_length = 300] @@ -139,7 +139,6 @@ diesel::joinable!(channel_permissions -> channels (channel_uuid)); diesel::joinable!(channels -> guilds (guild_uuid)); diesel::joinable!(guild_members -> guilds (guild_uuid)); diesel::joinable!(guild_members -> users (user_uuid)); -diesel::joinable!(guilds -> users (owner_uuid)); diesel::joinable!(instance_permissions -> users (uuid)); diesel::joinable!(invites -> guilds (guild_uuid)); diesel::joinable!(invites -> users (user_uuid)); From 95c942eee419c89a4dad069f307ca366890ebaaa Mon Sep 17 00:00:00 2001 From: Radical Date: Fri, 6 Jun 2025 17:49:06 +0200 Subject: [PATCH 061/160] feat: use permission system --- src/api/v1/channels/uuid/mod.rs | 14 ++-- src/api/v1/guilds/uuid/channels.rs | 10 +-- src/api/v1/guilds/uuid/icon.rs | 10 ++- src/api/v1/guilds/uuid/invites/mod.rs | 10 ++- src/api/v1/guilds/uuid/roles/mod.rs | 10 +-- src/objects/member.rs | 22 ++++-- src/objects/mod.rs | 39 +---------- src/objects/role.rs | 98 +++++++++++++++++++++++++-- 8 files changed, 133 insertions(+), 80 deletions(-) diff --git a/src/api/v1/channels/uuid/mod.rs b/src/api/v1/channels/uuid/mod.rs index 1cb20c7..bece6ed 100644 --- a/src/api/v1/channels/uuid/mod.rs +++ b/src/api/v1/channels/uuid/mod.rs @@ -4,11 +4,7 @@ pub mod messages; pub mod socket; use crate::{ - Data, - api::v1::auth::check_access_token, - error::Error, - objects::{Channel, Member}, - utils::{get_auth_header, global_checks}, + api::v1::auth::check_access_token, error::Error, objects::{Channel, Member, Permissions}, utils::{get_auth_header, global_checks}, Data }; use actix_web::{HttpRequest, HttpResponse, delete, get, patch, web}; use serde::Deserialize; @@ -59,7 +55,9 @@ pub async fn delete( let channel = Channel::fetch_one(&data, channel_uuid).await?; - Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; + let member = Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; + + member.check_permission(&data, Permissions::DeleteChannel).await?; channel.delete(&data).await?; @@ -125,7 +123,9 @@ pub async fn patch( let mut channel = Channel::fetch_one(&data, channel_uuid).await?; - Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; + let member = Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; + + member.check_permission(&data, Permissions::ManageChannel).await?; if let Some(new_name) = &new_info.name { channel.set_name(&data, new_name.to_string()).await?; diff --git a/src/api/v1/guilds/uuid/channels.rs b/src/api/v1/guilds/uuid/channels.rs index 083553a..db895e4 100644 --- a/src/api/v1/guilds/uuid/channels.rs +++ b/src/api/v1/guilds/uuid/channels.rs @@ -1,9 +1,5 @@ use crate::{ - Data, - api::v1::auth::check_access_token, - error::Error, - objects::{Channel, Member}, - utils::{get_auth_header, global_checks, order_by_is_above}, + api::v1::auth::check_access_token, error::Error, objects::{Channel, Member, Permissions}, utils::{get_auth_header, global_checks, order_by_is_above}, Data }; use ::uuid::Uuid; use actix_web::{HttpRequest, HttpResponse, get, post, web}; @@ -74,9 +70,9 @@ pub async fn create( global_checks(&data, uuid).await?; - Member::check_membership(&mut conn, uuid, guild_uuid).await?; + let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; - // FIXME: Logic to check permissions, should probably be done in utils.rs + member.check_permission(&data, Permissions::CreateChannel).await?; let channel = Channel::new( data.clone(), diff --git a/src/api/v1/guilds/uuid/icon.rs b/src/api/v1/guilds/uuid/icon.rs index 5025416..4061585 100644 --- a/src/api/v1/guilds/uuid/icon.rs +++ b/src/api/v1/guilds/uuid/icon.rs @@ -5,11 +5,7 @@ use futures_util::StreamExt as _; use uuid::Uuid; use crate::{ - Data, - api::v1::auth::check_access_token, - error::Error, - objects::{Guild, Member}, - utils::{get_auth_header, global_checks}, + api::v1::auth::check_access_token, error::Error, objects::{Guild, Member, Permissions}, utils::{get_auth_header, global_checks}, Data }; /// `PUT /api/v1/guilds/{uuid}/icon` Icon upload @@ -36,7 +32,9 @@ pub async fn upload( global_checks(&data, uuid).await?; - Member::check_membership(&mut conn, uuid, guild_uuid).await?; + let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; + + member.check_permission(&data, Permissions::ManageServer).await?; let mut guild = Guild::fetch_one(&mut conn, guild_uuid).await?; diff --git a/src/api/v1/guilds/uuid/invites/mod.rs b/src/api/v1/guilds/uuid/invites/mod.rs index bb8269c..eb8d2ce 100644 --- a/src/api/v1/guilds/uuid/invites/mod.rs +++ b/src/api/v1/guilds/uuid/invites/mod.rs @@ -3,11 +3,7 @@ use serde::Deserialize; use uuid::Uuid; use crate::{ - Data, - api::v1::auth::check_access_token, - error::Error, - objects::{Guild, Member}, - utils::{get_auth_header, global_checks}, + api::v1::auth::check_access_token, error::Error, objects::{Guild, Member, Permissions}, utils::{get_auth_header, global_checks}, Data }; #[derive(Deserialize)] @@ -61,7 +57,9 @@ pub async fn create( global_checks(&data, uuid).await?; - Member::check_membership(&mut conn, uuid, guild_uuid).await?; + let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; + + member.check_permission(&data, Permissions::CreateInvite).await?; let guild = Guild::fetch_one(&mut conn, guild_uuid).await?; diff --git a/src/api/v1/guilds/uuid/roles/mod.rs b/src/api/v1/guilds/uuid/roles/mod.rs index 717b30b..c33f144 100644 --- a/src/api/v1/guilds/uuid/roles/mod.rs +++ b/src/api/v1/guilds/uuid/roles/mod.rs @@ -3,11 +3,7 @@ use actix_web::{HttpRequest, HttpResponse, get, post, web}; use serde::Deserialize; use crate::{ - Data, - api::v1::auth::check_access_token, - error::Error, - objects::{Member, Role}, - utils::{get_auth_header, global_checks, order_by_is_above}, + api::v1::auth::check_access_token, error::Error, objects::{Member, Permissions, Role}, utils::{get_auth_header, global_checks, order_by_is_above}, Data }; pub mod uuid; @@ -70,9 +66,9 @@ pub async fn create( global_checks(&data, uuid).await?; - Member::check_membership(&mut conn, uuid, guild_uuid).await?; + let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; - // FIXME: Logic to check permissions, should probably be done in utils.rs + member.check_permission(&data, Permissions::CreateRole).await?; let role = Role::new(&mut conn, guild_uuid, role_info.name.clone()).await?; diff --git a/src/objects/member.rs b/src/objects/member.rs index 67312e4..20bc848 100644 --- a/src/objects/member.rs +++ b/src/objects/member.rs @@ -5,7 +5,7 @@ use diesel_async::RunQueryDsl; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::{Conn, Data, error::Error, schema::guild_members}; +use crate::{error::Error, objects::{Permissions, Role}, schema::guild_members, Conn, Data}; use super::{User, load_or_empty}; @@ -21,7 +21,7 @@ pub struct MemberBuilder { } impl MemberBuilder { - async fn build(&self, data: &Data) -> Result { + pub async fn build(&self, data: &Data) -> Result { let user = User::fetch_one(data, self.user_uuid).await?; Ok(Member { @@ -33,6 +33,18 @@ impl MemberBuilder { user, }) } + + pub async fn check_permission(&self, data: &Data, permission: Permissions) -> Result<(), Error> { + if !self.is_owner { + let roles = Role::fetch_from_member(&data, self.uuid).await?; + let allowed = roles.iter().any(|r| r.permissions & permission as i64 != 0); + if !allowed { + return Err(Error::Forbidden("Not allowed".to_string())) + } + } + + Ok(()) + } } #[derive(Serialize, Deserialize)] @@ -61,16 +73,16 @@ impl Member { conn: &mut Conn, user_uuid: Uuid, guild_uuid: Uuid, - ) -> Result<(), Error> { + ) -> Result { use guild_members::dsl; - dsl::guild_members + let member_builder = dsl::guild_members .filter(dsl::user_uuid.eq(user_uuid)) .filter(dsl::guild_uuid.eq(guild_uuid)) .select(MemberBuilder::as_select()) .get_result(conn) .await?; - Ok(()) + Ok(member_builder) } pub async fn fetch_one(data: &Data, user_uuid: Uuid, guild_uuid: Uuid) -> Result { diff --git a/src/objects/mod.rs b/src/objects/mod.rs index 7b45957..30a0a64 100644 --- a/src/objects/mod.rs +++ b/src/objects/mod.rs @@ -27,6 +27,7 @@ pub use member::Member; pub use message::Message; pub use password_reset_token::PasswordResetToken; pub use role::Role; +pub use role::Permissions; pub use user::User; use crate::error::Error; @@ -111,44 +112,6 @@ impl MailClient { } } -#[derive(Clone, Copy)] -pub enum Permissions { - SendMessage = 1, - CreateChannel = 2, - DeleteChannel = 4, - ManageChannel = 8, - CreateRole = 16, - DeleteRole = 32, - ManageRole = 64, - CreateInvite = 128, - ManageInvite = 256, - ManageServer = 512, - ManageMember = 1024, -} - -impl Permissions { - pub fn fetch_permissions(permissions: i64) -> Vec { - let all_perms = vec![ - Self::SendMessage, - Self::CreateChannel, - Self::DeleteChannel, - Self::ManageChannel, - Self::CreateRole, - Self::DeleteRole, - Self::ManageRole, - Self::CreateInvite, - Self::ManageInvite, - Self::ManageServer, - Self::ManageMember, - ]; - - all_perms - .into_iter() - .filter(|p| permissions & (*p as i64) != 0) - .collect() - } -} - #[derive(Deserialize)] pub struct StartAmountQuery { pub start: Option, diff --git a/src/objects/role.rs b/src/objects/role.rs index f67dc6d..a78798a 100644 --- a/src/objects/role.rs +++ b/src/objects/role.rs @@ -3,14 +3,14 @@ use diesel::{ update, }; use diesel_async::RunQueryDsl; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::{Conn, error::Error, schema::roles, utils::order_by_is_above}; +use crate::{error::Error, schema::{role_members, roles}, utils::order_by_is_above, Conn, Data}; use super::{HasIsAbove, HasUuid, load_or_empty}; -#[derive(Serialize, Clone, Queryable, Selectable, Insertable)] +#[derive(Deserialize, Serialize, Clone, Queryable, Selectable, Insertable)] #[diesel(table_name = roles)] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct Role { @@ -19,7 +19,28 @@ pub struct Role { name: String, color: i32, is_above: Option, - permissions: i64, + pub permissions: i64, +} + +#[derive(Serialize, Clone, Queryable, Selectable, Insertable)] +#[diesel(table_name = role_members)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct RoleMember { + role_uuid: Uuid, + member_uuid: Uuid, +} + +impl RoleMember { + async fn fetch_role(&self, conn: &mut Conn) -> Result { + use roles::dsl; + let role: Role = dsl::roles + .filter(dsl::uuid.eq(self.role_uuid)) + .select(Role::as_select()) + .get_result(conn) + .await?; + + Ok(role) + } } impl HasUuid for Role { @@ -48,6 +69,33 @@ impl Role { Ok(roles) } + pub async fn fetch_from_member(data: &Data, member_uuid: Uuid) -> Result, Error> { + if let Ok(roles) = data.get_cache_key(format!("{}_roles", member_uuid)).await { + return Ok(serde_json::from_str(&roles)?) + } + + let mut conn = data.pool.get().await?; + + use role_members::dsl; + let role_memberships: Vec = load_or_empty( + dsl::role_members + .filter(dsl::member_uuid.eq(member_uuid)) + .select(RoleMember::as_select()) + .load(&mut conn) + .await, + )?; + + let mut roles = vec![]; + + for membership in role_memberships { + roles.push(membership.fetch_role(&mut conn).await?); + } + + data.set_cache_key(format!("{}_roles", member_uuid), roles.clone(), 300).await?; + + Ok(roles) + } + pub async fn fetch_one(conn: &mut Conn, role_uuid: Uuid) -> Result { use roles::dsl; let role: Role = dsl::roles @@ -59,6 +107,10 @@ impl Role { Ok(role) } + pub async fn fetch_permissions(&self) -> Vec { + Permissions::fetch_permissions(self.permissions.clone()) + } + pub async fn new(conn: &mut Conn, guild_uuid: Uuid, name: String) -> Result { let role_uuid = Uuid::now_v7(); @@ -94,3 +146,41 @@ impl Role { Ok(new_role) } } + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum Permissions { + SendMessage = 1, + CreateChannel = 2, + DeleteChannel = 4, + ManageChannel = 8, + CreateRole = 16, + DeleteRole = 32, + ManageRole = 64, + CreateInvite = 128, + ManageInvite = 256, + ManageServer = 512, + ManageMember = 1024, +} + +impl Permissions { + pub fn fetch_permissions(permissions: i64) -> Vec { + let all_perms = vec![ + Self::SendMessage, + Self::CreateChannel, + Self::DeleteChannel, + Self::ManageChannel, + Self::CreateRole, + Self::DeleteRole, + Self::ManageRole, + Self::CreateInvite, + Self::ManageInvite, + Self::ManageServer, + Self::ManageMember, + ]; + + all_perms + .into_iter() + .filter(|p| permissions & (*p as i64) != 0) + .collect() + } +} From 8dca22de3a0a824dd318814a29c5c351ea0ced1d Mon Sep 17 00:00:00 2001 From: Radical Date: Fri, 6 Jun 2025 18:16:25 +0200 Subject: [PATCH 062/160] fix: make channel deletion work --- src/objects/channel.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/objects/channel.rs b/src/objects/channel.rs index 9b756f2..7a31150 100644 --- a/src/objects/channel.rs +++ b/src/objects/channel.rs @@ -198,15 +198,34 @@ impl Channel { let mut conn = data.pool.get().await?; use channels::dsl; + update(channels::table) + .filter(dsl::is_above.eq(self.uuid)) + .set(dsl::is_above.eq(None::)) + .execute(&mut conn) + .await?; delete(channels::table) .filter(dsl::uuid.eq(self.uuid)) .execute(&mut conn) .await?; + update(channels::table) + .filter(dsl::is_above.eq(self.uuid)) + .set(dsl::is_above.eq(self.is_above)) + .execute(&mut conn) + .await?; if data.get_cache_key(self.uuid.to_string()).await.is_ok() { data.del_cache_key(self.uuid.to_string()).await?; } + if data + .get_cache_key(format!("{}_channels", self.guild_uuid)) + .await + .is_ok() + { + data.del_cache_key(format!("{}_channels", self.guild_uuid)) + .await?; + } + Ok(()) } From f752cddd73d522b4011418ca3a5b75cdfd048388 Mon Sep 17 00:00:00 2001 From: Radical Date: Fri, 6 Jun 2025 18:19:40 +0200 Subject: [PATCH 063/160] fix: add missing match statements --- src/objects/channel.rs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/objects/channel.rs b/src/objects/channel.rs index 7a31150..4d52353 100644 --- a/src/objects/channel.rs +++ b/src/objects/channel.rs @@ -198,20 +198,32 @@ impl Channel { let mut conn = data.pool.get().await?; use channels::dsl; - update(channels::table) + match update(channels::table) .filter(dsl::is_above.eq(self.uuid)) .set(dsl::is_above.eq(None::)) .execute(&mut conn) - .await?; + .await + { + Ok(r) => Ok(r), + Err(diesel::result::Error::NotFound) => Ok(0), + Err(e) => Err(e), + }?; + delete(channels::table) .filter(dsl::uuid.eq(self.uuid)) .execute(&mut conn) .await?; - update(channels::table) + + match update(channels::table) .filter(dsl::is_above.eq(self.uuid)) .set(dsl::is_above.eq(self.is_above)) .execute(&mut conn) - .await?; + .await + { + Ok(r) => Ok(r), + Err(diesel::result::Error::NotFound) => Ok(0), + Err(e) => Err(e), + }?; if data.get_cache_key(self.uuid.to_string()).await.is_ok() { data.del_cache_key(self.uuid.to_string()).await?; From 407460d2aa1391ac8a50731f970fb8c4622c589a Mon Sep 17 00:00:00 2001 From: Radical Date: Wed, 25 Jun 2025 13:25:39 +0200 Subject: [PATCH 064/160] style: use const generic for token length instead of multiple functions Simplifies codebase a bit and avoids having to add another function in future if we need another length of token --- src/api/v1/auth/login.rs | 6 +++--- src/api/v1/auth/refresh.rs | 6 +++--- src/api/v1/auth/register.rs | 6 +++--- src/objects/email_token.rs | 4 ++-- src/objects/password_reset_token.rs | 6 +++--- src/utils.rs | 10 ++-------- 6 files changed, 16 insertions(+), 22 deletions(-) diff --git a/src/api/v1/auth/login.rs b/src/api/v1/auth/login.rs index e190c2f..ac6c1ad 100644 --- a/src/api/v1/auth/login.rs +++ b/src/api/v1/auth/login.rs @@ -11,7 +11,7 @@ use crate::{ error::Error, schema::*, utils::{ - PASSWORD_REGEX, generate_access_token, generate_refresh_token, new_refresh_token_cookie, + PASSWORD_REGEX, generate_token, new_refresh_token_cookie, user_uuid_from_identifier, }, }; @@ -59,8 +59,8 @@ pub async fn response( )); } - let refresh_token = generate_refresh_token()?; - let access_token = generate_access_token()?; + let refresh_token = generate_token::<32>()?; + let access_token = generate_token::<16>()?; let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; diff --git a/src/api/v1/auth/refresh.rs b/src/api/v1/auth/refresh.rs index 63e150e..1f4f406 100644 --- a/src/api/v1/auth/refresh.rs +++ b/src/api/v1/auth/refresh.rs @@ -11,7 +11,7 @@ use crate::{ access_tokens::{self, dsl}, refresh_tokens::{self, dsl as rdsl}, }, - utils::{generate_access_token, generate_refresh_token, new_refresh_token_cookie}, + utils::{generate_token, new_refresh_token_cookie}, }; use super::Response; @@ -55,7 +55,7 @@ pub async fn res(req: HttpRequest, data: web::Data) -> Result 1987200 { - let new_refresh_token = generate_refresh_token()?; + let new_refresh_token = generate_token::<32>()?; match update(refresh_tokens::table) .filter(rdsl::token.eq(&refresh_token)) @@ -75,7 +75,7 @@ pub async fn res(req: HttpRequest, data: web::Data) -> Result()?; update(access_tokens::table) .filter(dsl::refresh_token.eq(&refresh_token)) diff --git a/src/api/v1/auth/register.rs b/src/api/v1/auth/register.rs index 66e2989..1d28088 100644 --- a/src/api/v1/auth/register.rs +++ b/src/api/v1/auth/register.rs @@ -20,7 +20,7 @@ use crate::{ users::{self, dsl as udsl}, }, utils::{ - EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX, generate_access_token, generate_refresh_token, + EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX, generate_token, new_refresh_token_cookie, }, }; @@ -120,8 +120,8 @@ pub async fn res( .execute(&mut conn) .await?; - let refresh_token = generate_refresh_token()?; - let access_token = generate_access_token()?; + let refresh_token = generate_token::<32>()?; + let access_token = generate_token::<16>()?; let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; diff --git a/src/objects/email_token.rs b/src/objects/email_token.rs index f55de8c..4ec6b7e 100644 --- a/src/objects/email_token.rs +++ b/src/objects/email_token.rs @@ -3,7 +3,7 @@ use lettre::message::MultiPart; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::{Data, error::Error, utils::generate_refresh_token}; +use crate::{Data, error::Error, utils::generate_token}; use super::Me; @@ -23,7 +23,7 @@ impl EmailToken { #[allow(clippy::new_ret_no_self)] pub async fn new(data: &Data, me: Me) -> Result<(), Error> { - let token = generate_refresh_token()?; + let token = generate_token::<32>()?; let email_token = EmailToken { user_uuid: me.uuid, diff --git a/src/objects/password_reset_token.rs b/src/objects/password_reset_token.rs index 0376d88..e14d25a 100644 --- a/src/objects/password_reset_token.rs +++ b/src/objects/password_reset_token.rs @@ -12,10 +12,10 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ - Data, error::Error, schema::users, - utils::{PASSWORD_REGEX, generate_refresh_token, global_checks, user_uuid_from_identifier}, + utils::{generate_token, global_checks, user_uuid_from_identifier, PASSWORD_REGEX}, + Data }; #[derive(Serialize, Deserialize)] @@ -48,7 +48,7 @@ impl PasswordResetToken { #[allow(clippy::new_ret_no_self)] pub async fn new(data: &Data, identifier: String) -> Result<(), Error> { - let token = generate_refresh_token()?; + let token = generate_token::<32>()?; let mut conn = data.pool.get().await?; diff --git a/src/utils.rs b/src/utils.rs index 3172cec..7a5581a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -115,14 +115,8 @@ pub fn new_refresh_token_cookie(config: &Config, refresh_token: String) -> Cooki .finish() } -pub fn generate_access_token() -> Result { - let mut buf = [0u8; 16]; - fill(&mut buf)?; - Ok(encode(buf)) -} - -pub fn generate_refresh_token() -> Result { - let mut buf = [0u8; 32]; +pub fn generate_token() -> Result { + let mut buf = [0u8; N]; fill(&mut buf)?; Ok(encode(buf)) } From 36d3a18b0857691765c0d803dceef0f427f44bf4 Mon Sep 17 00:00:00 2001 From: Radical Date: Wed, 25 Jun 2025 14:33:05 +0200 Subject: [PATCH 065/160] build: update dependencies --- Cargo.toml | 8 ++++---- src/api/v1/guilds/uuid/icon.rs | 2 +- src/main.rs | 11 +++-------- src/objects/guild.rs | 6 +++--- src/objects/me.rs | 5 ++--- 5 files changed, 13 insertions(+), 19 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 30b5827..1c5f34b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,15 +27,15 @@ regex = "1.11" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" simple_logger = "5.0.0" -redis = { version = "0.31.0", features= ["tokio-comp"] } -tokio-tungstenite = { version = "0.26", features = ["native-tls", "url"] } +redis = { version = "0.32", features= ["tokio-comp"] } +tokio-tungstenite = { version = "0.27", features = ["native-tls", "url"] } toml = "0.8" url = { version = "2.5", features = ["serde"] } uuid = { version = "1.17", features = ["serde", "v7"] } random-string = "1.1" actix-ws = "0.3.0" futures-util = "0.3.31" -bunny-api-tokio = "0.3.0" +bunny-api-tokio = { version = "0.4", features = ["edge_storage"], default-features = false } bindet = "0.3.2" deadpool = "0.12" diesel = { version = "2.2", features = ["uuid", "chrono"], default-features = false } @@ -43,7 +43,7 @@ diesel-async = { version = "0.5", features = ["deadpool", "postgres", "async-con diesel_migrations = { version = "2.2.0", features = ["postgres"] } thiserror = "2.0.12" actix-multipart = "0.7.2" -lettre = { version = "0.11.16", features = ["tokio1", "tokio1-native-tls"] } +lettre = { version = "0.11", features = ["tokio1", "tokio1-native-tls"] } chrono = { version = "0.4.41", features = ["serde"] } [dependencies.tokio] diff --git a/src/api/v1/guilds/uuid/icon.rs b/src/api/v1/guilds/uuid/icon.rs index 4061585..0860435 100644 --- a/src/api/v1/guilds/uuid/icon.rs +++ b/src/api/v1/guilds/uuid/icon.rs @@ -45,7 +45,7 @@ pub async fn upload( guild .set_icon( - &data.bunny_cdn, + &data.bunny_storage, &mut conn, data.config.bunny.cdn_url.clone(), bytes, diff --git a/src/main.rs b/src/main.rs index 540a237..47794e3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,7 +40,7 @@ pub struct Data { pub config: Config, pub argon2: Argon2<'static>, pub start_time: SystemTime, - pub bunny_cdn: bunny_api_tokio::Client, + pub bunny_storage: bunny_api_tokio::EdgeStorageClient, pub mail_client: MailClient, } @@ -65,14 +65,9 @@ async fn main() -> Result<(), Error> { let cache_pool = redis::Client::open(config.cache_database.url())?; - let mut bunny_cdn = bunny_api_tokio::Client::new("").await?; - let bunny = config.bunny.clone(); - bunny_cdn - .storage - .init(bunny.api_key, bunny.endpoint, bunny.storage_zone) - .await?; + let bunny_storage = bunny_api_tokio::EdgeStorageClient::new(bunny.api_key, bunny.endpoint, bunny.storage_zone).await?; let mail = config.mail.clone(); @@ -122,7 +117,7 @@ async fn main() -> Result<(), Error> { // TODO: Possibly implement "pepper" into this (thinking it could generate one if it doesnt exist and store it on disk) argon2: Argon2::default(), start_time: SystemTime::now(), - bunny_cdn, + bunny_storage, mail_client, }; diff --git a/src/objects/guild.rs b/src/objects/guild.rs index 7d55595..47058ee 100644 --- a/src/objects/guild.rs +++ b/src/objects/guild.rs @@ -188,7 +188,7 @@ impl Guild { // FIXME: Horrible security pub async fn set_icon( &mut self, - bunny_cdn: &bunny_api_tokio::Client, + bunny_storage: &bunny_api_tokio::EdgeStorageClient, conn: &mut Conn, cdn_url: Url, icon: BytesMut, @@ -199,12 +199,12 @@ impl Guild { if let Some(icon) = &self.icon { let relative_url = icon.path().trim_start_matches('/'); - bunny_cdn.storage.delete(relative_url).await?; + bunny_storage.delete(relative_url).await?; } let path = format!("icons/{}/icon.{}", self.uuid, image_type); - bunny_cdn.storage.upload(path.clone(), icon.into()).await?; + bunny_storage.upload(path.clone(), icon.into()).await?; let icon_url = cdn_url.join(&path)?; diff --git a/src/objects/me.rs b/src/objects/me.rs index 6af5bce..e183c5d 100644 --- a/src/objects/me.rs +++ b/src/objects/me.rs @@ -85,13 +85,12 @@ impl Me { let relative_url = avatar_url.path().trim_start_matches('/'); - data.bunny_cdn.storage.delete(relative_url).await?; + data.bunny_storage.delete(relative_url).await?; } let path = format!("avatar/{}/avatar.{}", self.uuid, image_type); - data.bunny_cdn - .storage + data.bunny_storage .upload(path.clone(), avatar.into()) .await?; From 243c496fda9570d596d5fa1ce5344ff5f8488aaa Mon Sep 17 00:00:00 2001 From: Radical Date: Wed, 2 Jul 2025 20:12:22 +0200 Subject: [PATCH 066/160] fix: use a UUIDv7 as filename for images Fixes problem with caching in bunny.net --- src/objects/guild.rs | 2 +- src/objects/me.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/objects/guild.rs b/src/objects/guild.rs index 47058ee..aa01f54 100644 --- a/src/objects/guild.rs +++ b/src/objects/guild.rs @@ -202,7 +202,7 @@ impl Guild { bunny_storage.delete(relative_url).await?; } - let path = format!("icons/{}/icon.{}", self.uuid, image_type); + let path = format!("icons/{}/{}.{}", self.uuid, Uuid::now_v7(), image_type); bunny_storage.upload(path.clone(), icon.into()).await?; diff --git a/src/objects/me.rs b/src/objects/me.rs index e183c5d..d99a7b4 100644 --- a/src/objects/me.rs +++ b/src/objects/me.rs @@ -88,7 +88,7 @@ impl Me { data.bunny_storage.delete(relative_url).await?; } - let path = format!("avatar/{}/avatar.{}", self.uuid, image_type); + let path = format!("avatar/{}/{}.{}", self.uuid, Uuid::now_v7(), image_type); data.bunny_storage .upload(path.clone(), avatar.into()) From 6eb47fdb363194f95a4e5240c5f774d9d76d2f1b Mon Sep 17 00:00:00 2001 From: Radical Date: Wed, 2 Jul 2025 20:27:22 +0200 Subject: [PATCH 067/160] build: update & sort dependencies --- Cargo.toml | 57 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1c5f34b..c1c71bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,37 +15,50 @@ strip = "debuginfo" codegen-units = 512 [dependencies] -actix-cors = "0.7.1" -actix-web = "4.11" -argon2 = { version = "0.5.3", features = ["std"] } +thiserror = "2.0.12" + +# CLI clap = { version = "4.5", features = ["derive"] } -futures = "0.3" -getrandom = "0.3" -hex = "0.4" log = "0.4" -regex = "1.11" +simple_logger = "5.0.0" + +# async +futures = "0.3" +tokio = { version = "1.46", features = ["full"] } +futures-util = "0.3.31" + +# Data (de)serialization serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -simple_logger = "5.0.0" -redis = { version = "0.32", features= ["tokio-comp"] } -tokio-tungstenite = { version = "0.27", features = ["native-tls", "url"] } toml = "0.8" -url = { version = "2.5", features = ["serde"] } -uuid = { version = "1.17", features = ["serde", "v7"] } -random-string = "1.1" -actix-ws = "0.3.0" -futures-util = "0.3.31" -bunny-api-tokio = { version = "0.4", features = ["edge_storage"], default-features = false } + +# File Storage bindet = "0.3.2" +bunny-api-tokio = { version = "0.4", features = ["edge_storage"], default-features = false } + +# Web Server +actix-web = "4.11" +actix-cors = "0.7.1" +actix-ws = "0.3.0" +actix-multipart = "0.7.2" +url = { version = "2.5", features = ["serde"] } +tokio-tungstenite = { version = "0.27", features = ["native-tls", "url"] } + +# Database +uuid = { version = "1.17", features = ["serde", "v7"] } +redis = { version = "0.32", features= ["tokio-comp"] } deadpool = "0.12" diesel = { version = "2.2", features = ["uuid", "chrono"], default-features = false } -diesel-async = { version = "0.5", features = ["deadpool", "postgres", "async-connection-wrapper"] } +diesel-async = { version = "0.6", features = ["deadpool", "postgres", "async-connection-wrapper"] } diesel_migrations = { version = "2.2.0", features = ["postgres"] } -thiserror = "2.0.12" -actix-multipart = "0.7.2" + +# Authentication +argon2 = { version = "0.5.3", features = ["std"] } +getrandom = "0.3" +hex = "0.4" +regex = "1.11" +random-string = "1.1" lettre = { version = "0.11", features = ["tokio1", "tokio1-native-tls"] } chrono = { version = "0.4.41", features = ["serde"] } -[dependencies.tokio] -version = "1.45" -features = ["full"] + From 19f64d413c86c6613858babbd972737966b958e1 Mon Sep 17 00:00:00 2001 From: Radical Date: Wed, 2 Jul 2025 20:39:12 +0200 Subject: [PATCH 068/160] feat: make it possible to automatically join user to a guild on registration --- src/api/v1/auth/register.rs | 20 +++++++++----------- src/config.rs | 5 +++++ 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/api/v1/auth/register.rs b/src/api/v1/auth/register.rs index 1d28088..49829a1 100644 --- a/src/api/v1/auth/register.rs +++ b/src/api/v1/auth/register.rs @@ -12,17 +12,11 @@ use uuid::Uuid; use super::Response; use crate::{ - Data, - error::Error, - schema::{ - access_tokens::{self, dsl as adsl}, - refresh_tokens::{self, dsl as rdsl}, - users::{self, dsl as udsl}, - }, - utils::{ - EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX, generate_token, - new_refresh_token_cookie, - }, + error::Error, objects::Member, schema::{ + access_tokens::{self, dsl as adsl}, refresh_tokens::{self, dsl as rdsl}, users::{self, dsl as udsl} + }, utils::{ + generate_token, new_refresh_token_cookie, EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX + }, Data }; #[derive(Deserialize)] @@ -145,6 +139,10 @@ pub async fn res( .execute(&mut conn) .await?; + if let Some(initial_guild) = data.config.instance.initial_guild { + Member::new(&data, uuid, initial_guild).await?; + } + return Ok(HttpResponse::Ok() .cookie(new_refresh_token_cookie(&data.config, refresh_token)) .json(Response { access_token })); diff --git a/src/config.rs b/src/config.rs index cbcc8c5..7c7adb6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,6 +5,7 @@ use log::debug; use serde::Deserialize; use tokio::fs::read_to_string; use url::Url; +use uuid::Uuid; #[derive(Debug, Deserialize)] pub struct ConfigBuilder { @@ -48,6 +49,7 @@ struct InstanceBuilder { name: Option, registration: Option, require_email_verification: Option, + initial_guild: Option, } #[derive(Debug, Deserialize)] @@ -119,11 +121,13 @@ impl ConfigBuilder { name: instance.name.unwrap_or("Gorb".to_string()), registration: instance.registration.unwrap_or(true), require_email_verification: instance.require_email_verification.unwrap_or(false), + initial_guild: instance.initial_guild, }, None => Instance { name: "Gorb".to_string(), registration: true, require_email_verification: false, + initial_guild: None, }, }; @@ -161,6 +165,7 @@ pub struct Instance { pub name: String, pub registration: bool, pub require_email_verification: bool, + pub initial_guild: Option, } #[derive(Debug, Clone)] From e59f7b5a1f655522f1024ae5331ed76e7ed31ea1 Mon Sep 17 00:00:00 2001 From: Radical Date: Wed, 2 Jul 2025 20:47:59 +0200 Subject: [PATCH 069/160] style: cargo clippy & cargo fmt --- build.rs | 2 +- src/api/v1/auth/login.rs | 5 +--- src/api/v1/auth/refresh.rs | 4 +-- src/api/v1/auth/register.rs | 16 +++++++---- src/api/v1/auth/reset_password.rs | 3 +-- src/api/v1/channels/uuid/mod.rs | 14 +++++++--- src/api/v1/guilds/uuid/channels.rs | 14 +++++++--- src/api/v1/guilds/uuid/icon.rs | 10 +++++-- src/api/v1/guilds/uuid/invites/mod.rs | 14 +++++++--- src/api/v1/guilds/uuid/roles/mod.rs | 14 +++++++--- src/api/v1/guilds/uuid/roles/uuid.rs | 4 +-- src/config.rs | 2 +- src/error.rs | 2 +- src/main.rs | 4 ++- src/objects/channel.rs | 5 ++-- src/objects/email_token.rs | 16 +++++++---- src/objects/member.rs | 17 +++++++++--- src/objects/mod.rs | 4 +-- src/objects/password_reset_token.rs | 38 ++++++++++++++++++--------- src/objects/role.rs | 16 +++++++---- 20 files changed, 137 insertions(+), 67 deletions(-) diff --git a/build.rs b/build.rs index 45de5ff..55fb863 100644 --- a/build.rs +++ b/build.rs @@ -12,5 +12,5 @@ fn main() { .unwrap_or_else(|| "UNKNOWN".to_string()); // Tell Cargo to set `GIT_SHORT_HASH` for the main compilation - println!("cargo:rustc-env=GIT_SHORT_HASH={}", git_short_hash); + println!("cargo:rustc-env=GIT_SHORT_HASH={git_short_hash}"); } diff --git a/src/api/v1/auth/login.rs b/src/api/v1/auth/login.rs index ac6c1ad..2faaeb4 100644 --- a/src/api/v1/auth/login.rs +++ b/src/api/v1/auth/login.rs @@ -10,10 +10,7 @@ use crate::{ Data, error::Error, schema::*, - utils::{ - PASSWORD_REGEX, generate_token, new_refresh_token_cookie, - user_uuid_from_identifier, - }, + utils::{PASSWORD_REGEX, generate_token, new_refresh_token_cookie, user_uuid_from_identifier}, }; use super::Response; diff --git a/src/api/v1/auth/refresh.rs b/src/api/v1/auth/refresh.rs index 1f4f406..abd9a34 100644 --- a/src/api/v1/auth/refresh.rs +++ b/src/api/v1/auth/refresh.rs @@ -42,7 +42,7 @@ pub async fn res(req: HttpRequest, data: web::Data) -> Result) -> Result { - error!("{}", error); + error!("{error}"); } } } diff --git a/src/api/v1/auth/register.rs b/src/api/v1/auth/register.rs index 49829a1..e57a1ae 100644 --- a/src/api/v1/auth/register.rs +++ b/src/api/v1/auth/register.rs @@ -12,11 +12,17 @@ use uuid::Uuid; use super::Response; use crate::{ - error::Error, objects::Member, schema::{ - access_tokens::{self, dsl as adsl}, refresh_tokens::{self, dsl as rdsl}, users::{self, dsl as udsl} - }, utils::{ - generate_token, new_refresh_token_cookie, EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX - }, Data + Data, + error::Error, + objects::Member, + schema::{ + access_tokens::{self, dsl as adsl}, + refresh_tokens::{self, dsl as rdsl}, + users::{self, dsl as udsl}, + }, + utils::{ + EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX, generate_token, new_refresh_token_cookie, + }, }; #[derive(Deserialize)] diff --git a/src/api/v1/auth/reset_password.rs b/src/api/v1/auth/reset_password.rs index 444266c..9a4497f 100644 --- a/src/api/v1/auth/reset_password.rs +++ b/src/api/v1/auth/reset_password.rs @@ -72,8 +72,7 @@ pub async fn post( reset_password: web::Json, data: web::Data, ) -> Result { - let password_reset_token = - PasswordResetToken::get(&data, reset_password.token.clone()).await?; + let password_reset_token = PasswordResetToken::get(&data, reset_password.token.clone()).await?; password_reset_token .set_password(&data, reset_password.password.clone()) diff --git a/src/api/v1/channels/uuid/mod.rs b/src/api/v1/channels/uuid/mod.rs index bece6ed..d7cfa39 100644 --- a/src/api/v1/channels/uuid/mod.rs +++ b/src/api/v1/channels/uuid/mod.rs @@ -4,7 +4,11 @@ pub mod messages; pub mod socket; use crate::{ - api::v1::auth::check_access_token, error::Error, objects::{Channel, Member, Permissions}, utils::{get_auth_header, global_checks}, Data + Data, + api::v1::auth::check_access_token, + error::Error, + objects::{Channel, Member, Permissions}, + utils::{get_auth_header, global_checks}, }; use actix_web::{HttpRequest, HttpResponse, delete, get, patch, web}; use serde::Deserialize; @@ -57,7 +61,9 @@ pub async fn delete( let member = Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; - member.check_permission(&data, Permissions::DeleteChannel).await?; + member + .check_permission(&data, Permissions::DeleteChannel) + .await?; channel.delete(&data).await?; @@ -125,7 +131,9 @@ pub async fn patch( let member = Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; - member.check_permission(&data, Permissions::ManageChannel).await?; + member + .check_permission(&data, Permissions::ManageChannel) + .await?; if let Some(new_name) = &new_info.name { channel.set_name(&data, new_name.to_string()).await?; diff --git a/src/api/v1/guilds/uuid/channels.rs b/src/api/v1/guilds/uuid/channels.rs index db895e4..8fda917 100644 --- a/src/api/v1/guilds/uuid/channels.rs +++ b/src/api/v1/guilds/uuid/channels.rs @@ -1,5 +1,9 @@ use crate::{ - api::v1::auth::check_access_token, error::Error, objects::{Channel, Member, Permissions}, utils::{get_auth_header, global_checks, order_by_is_above}, Data + Data, + api::v1::auth::check_access_token, + error::Error, + objects::{Channel, Member, Permissions}, + utils::{get_auth_header, global_checks, order_by_is_above}, }; use ::uuid::Uuid; use actix_web::{HttpRequest, HttpResponse, get, post, web}; @@ -31,7 +35,7 @@ pub async fn get( Member::check_membership(&mut conn, uuid, guild_uuid).await?; - if let Ok(cache_hit) = data.get_cache_key(format!("{}_channels", guild_uuid)).await { + if let Ok(cache_hit) = data.get_cache_key(format!("{guild_uuid}_channels")).await { return Ok(HttpResponse::Ok() .content_type("application/json") .body(cache_hit)); @@ -42,7 +46,7 @@ pub async fn get( let channels_ordered = order_by_is_above(channels).await?; data.set_cache_key( - format!("{}_channels", guild_uuid), + format!("{guild_uuid}_channels"), channels_ordered.clone(), 1800, ) @@ -72,7 +76,9 @@ pub async fn create( let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; - member.check_permission(&data, Permissions::CreateChannel).await?; + member + .check_permission(&data, Permissions::CreateChannel) + .await?; let channel = Channel::new( data.clone(), diff --git a/src/api/v1/guilds/uuid/icon.rs b/src/api/v1/guilds/uuid/icon.rs index 0860435..43a5e05 100644 --- a/src/api/v1/guilds/uuid/icon.rs +++ b/src/api/v1/guilds/uuid/icon.rs @@ -5,7 +5,11 @@ use futures_util::StreamExt as _; use uuid::Uuid; use crate::{ - api::v1::auth::check_access_token, error::Error, objects::{Guild, Member, Permissions}, utils::{get_auth_header, global_checks}, Data + Data, + api::v1::auth::check_access_token, + error::Error, + objects::{Guild, Member, Permissions}, + utils::{get_auth_header, global_checks}, }; /// `PUT /api/v1/guilds/{uuid}/icon` Icon upload @@ -34,7 +38,9 @@ pub async fn upload( let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; - member.check_permission(&data, Permissions::ManageServer).await?; + member + .check_permission(&data, Permissions::ManageServer) + .await?; let mut guild = Guild::fetch_one(&mut conn, guild_uuid).await?; diff --git a/src/api/v1/guilds/uuid/invites/mod.rs b/src/api/v1/guilds/uuid/invites/mod.rs index eb8d2ce..f1c62bc 100644 --- a/src/api/v1/guilds/uuid/invites/mod.rs +++ b/src/api/v1/guilds/uuid/invites/mod.rs @@ -3,7 +3,11 @@ use serde::Deserialize; use uuid::Uuid; use crate::{ - api::v1::auth::check_access_token, error::Error, objects::{Guild, Member, Permissions}, utils::{get_auth_header, global_checks}, Data + Data, + api::v1::auth::check_access_token, + error::Error, + objects::{Guild, Member, Permissions}, + utils::{get_auth_header, global_checks}, }; #[derive(Deserialize)] @@ -59,11 +63,15 @@ pub async fn create( let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; - member.check_permission(&data, Permissions::CreateInvite).await?; + member + .check_permission(&data, Permissions::CreateInvite) + .await?; let guild = Guild::fetch_one(&mut conn, guild_uuid).await?; - let invite = guild.create_invite(&mut conn, uuid, invite_request.custom_id.clone()).await?; + let invite = guild + .create_invite(&mut conn, uuid, invite_request.custom_id.clone()) + .await?; Ok(HttpResponse::Ok().json(invite)) } diff --git a/src/api/v1/guilds/uuid/roles/mod.rs b/src/api/v1/guilds/uuid/roles/mod.rs index c33f144..5f38923 100644 --- a/src/api/v1/guilds/uuid/roles/mod.rs +++ b/src/api/v1/guilds/uuid/roles/mod.rs @@ -3,7 +3,11 @@ use actix_web::{HttpRequest, HttpResponse, get, post, web}; use serde::Deserialize; use crate::{ - api::v1::auth::check_access_token, error::Error, objects::{Member, Permissions, Role}, utils::{get_auth_header, global_checks, order_by_is_above}, Data + Data, + api::v1::auth::check_access_token, + error::Error, + objects::{Member, Permissions, Role}, + utils::{get_auth_header, global_checks, order_by_is_above}, }; pub mod uuid; @@ -31,7 +35,7 @@ pub async fn get( Member::check_membership(&mut conn, uuid, guild_uuid).await?; - if let Ok(cache_hit) = data.get_cache_key(format!("{}_roles", guild_uuid)).await { + if let Ok(cache_hit) = data.get_cache_key(format!("{guild_uuid}_roles")).await { return Ok(HttpResponse::Ok() .content_type("application/json") .body(cache_hit)); @@ -41,7 +45,7 @@ pub async fn get( let roles_ordered = order_by_is_above(roles).await?; - data.set_cache_key(format!("{}_roles", guild_uuid), roles_ordered.clone(), 1800) + data.set_cache_key(format!("{guild_uuid}_roles"), roles_ordered.clone(), 1800) .await?; Ok(HttpResponse::Ok().json(roles_ordered)) @@ -68,7 +72,9 @@ pub async fn create( let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; - member.check_permission(&data, Permissions::CreateRole).await?; + member + .check_permission(&data, Permissions::CreateRole) + .await?; let role = Role::new(&mut conn, guild_uuid, role_info.name.clone()).await?; diff --git a/src/api/v1/guilds/uuid/roles/uuid.rs b/src/api/v1/guilds/uuid/roles/uuid.rs index f1a3206..bd747d8 100644 --- a/src/api/v1/guilds/uuid/roles/uuid.rs +++ b/src/api/v1/guilds/uuid/roles/uuid.rs @@ -28,7 +28,7 @@ pub async fn get( Member::check_membership(&mut conn, uuid, guild_uuid).await?; - if let Ok(cache_hit) = data.get_cache_key(format!("{}", role_uuid)).await { + if let Ok(cache_hit) = data.get_cache_key(format!("{role_uuid}")).await { return Ok(HttpResponse::Ok() .content_type("application/json") .body(cache_hit)); @@ -36,7 +36,7 @@ pub async fn get( let role = Role::fetch_one(&mut conn, role_uuid).await?; - data.set_cache_key(format!("{}", role_uuid), role.clone(), 60) + data.set_cache_key(format!("{role_uuid}"), role.clone(), 60) .await?; Ok(HttpResponse::Ok().json(role)) diff --git a/src/config.rs b/src/config.rs index 7c7adb6..2d2c8e0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -76,7 +76,7 @@ pub struct Smtp { impl ConfigBuilder { pub async fn load(path: String) -> Result { - debug!("loading config from: {}", path); + debug!("loading config from: {path}"); let raw = read_to_string(path).await?; let config = toml::from_str(&raw)?; diff --git a/src/error.rs b/src/error.rs index 1b1bfba..35b533d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -79,7 +79,7 @@ pub enum Error { impl ResponseError for Error { fn error_response(&self) -> HttpResponse { - debug!("{:?}", self); + debug!("{self:?}"); error!("{}: {}", self.status_code(), self); HttpResponse::build(self.status_code()) diff --git a/src/main.rs b/src/main.rs index 47794e3..248289a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -67,7 +67,9 @@ async fn main() -> Result<(), Error> { let bunny = config.bunny.clone(); - let bunny_storage = bunny_api_tokio::EdgeStorageClient::new(bunny.api_key, bunny.endpoint, bunny.storage_zone).await?; + let bunny_storage = + bunny_api_tokio::EdgeStorageClient::new(bunny.api_key, bunny.endpoint, bunny.storage_zone) + .await?; let mail = config.mail.clone(); diff --git a/src/objects/channel.rs b/src/objects/channel.rs index 4d52353..7d21a1d 100644 --- a/src/objects/channel.rs +++ b/src/objects/channel.rs @@ -183,12 +183,11 @@ impl Channel { .await?; if data - .get_cache_key(format!("{}_channels", guild_uuid)) + .get_cache_key(format!("{guild_uuid}_channels")) .await .is_ok() { - data.del_cache_key(format!("{}_channels", guild_uuid)) - .await?; + data.del_cache_key(format!("{guild_uuid}_channels")).await?; } Ok(channel) diff --git a/src/objects/email_token.rs b/src/objects/email_token.rs index 4ec6b7e..bfd1ef5 100644 --- a/src/objects/email_token.rs +++ b/src/objects/email_token.rs @@ -16,7 +16,11 @@ pub struct EmailToken { impl EmailToken { pub async fn get(data: &Data, user_uuid: Uuid) -> Result { - let email_token = serde_json::from_str(&data.get_cache_key(format!("{}_email_verify", user_uuid)).await?)?; + let email_token = serde_json::from_str( + &data + .get_cache_key(format!("{user_uuid}_email_verify")) + .await?, + )?; Ok(email_token) } @@ -29,14 +33,15 @@ impl EmailToken { user_uuid: me.uuid, token: token.clone(), // TODO: Check if this can be replaced with something built into valkey - created_at: Utc::now() + created_at: Utc::now(), }; - data.set_cache_key(format!("{}_email_verify", me.uuid), email_token, 86400).await?; + data.set_cache_key(format!("{}_email_verify", me.uuid), email_token, 86400) + .await?; let mut verify_endpoint = data.config.web.frontend_url.join("verify-email")?; - verify_endpoint.set_query(Some(&format!("token={}", token))); + verify_endpoint.set_query(Some(&format!("token={token}"))); let email = data .mail_client @@ -54,7 +59,8 @@ impl EmailToken { } pub async fn delete(&self, data: &Data) -> Result<(), Error> { - data.del_cache_key(format!("{}_email_verify", self.user_uuid)).await?; + data.del_cache_key(format!("{}_email_verify", self.user_uuid)) + .await?; Ok(()) } diff --git a/src/objects/member.rs b/src/objects/member.rs index 20bc848..d33d2b6 100644 --- a/src/objects/member.rs +++ b/src/objects/member.rs @@ -5,7 +5,12 @@ use diesel_async::RunQueryDsl; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::{error::Error, objects::{Permissions, Role}, schema::guild_members, Conn, Data}; +use crate::{ + Conn, Data, + error::Error, + objects::{Permissions, Role}, + schema::guild_members, +}; use super::{User, load_or_empty}; @@ -34,12 +39,16 @@ impl MemberBuilder { }) } - pub async fn check_permission(&self, data: &Data, permission: Permissions) -> Result<(), Error> { + pub async fn check_permission( + &self, + data: &Data, + permission: Permissions, + ) -> Result<(), Error> { if !self.is_owner { - let roles = Role::fetch_from_member(&data, self.uuid).await?; + let roles = Role::fetch_from_member(data, self.uuid).await?; let allowed = roles.iter().any(|r| r.permissions & permission as i64 != 0); if !allowed { - return Err(Error::Forbidden("Not allowed".to_string())) + return Err(Error::Forbidden("Not allowed".to_string())); } } diff --git a/src/objects/mod.rs b/src/objects/mod.rs index 30a0a64..d8de266 100644 --- a/src/objects/mod.rs +++ b/src/objects/mod.rs @@ -26,8 +26,8 @@ pub use me::Me; pub use member::Member; pub use message::Message; pub use password_reset_token::PasswordResetToken; -pub use role::Role; pub use role::Permissions; +pub use role::Role; pub use user::User; use crate::error::Error; @@ -106,7 +106,7 @@ impl MailClient { let response = mailer.send(email).await?; - debug!("mail sending response: {:?}", response); + debug!("mail sending response: {response:?}"); Ok(()) } diff --git a/src/objects/password_reset_token.rs b/src/objects/password_reset_token.rs index e14d25a..7f714ef 100644 --- a/src/objects/password_reset_token.rs +++ b/src/objects/password_reset_token.rs @@ -3,19 +3,17 @@ use argon2::{ password_hash::{SaltString, rand_core::OsRng}, }; use chrono::Utc; -use diesel::{ - ExpressionMethods, QueryDsl, update, -}; +use diesel::{ExpressionMethods, QueryDsl, update}; use diesel_async::RunQueryDsl; use lettre::message::MultiPart; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ + Data, error::Error, schema::users, - utils::{generate_token, global_checks, user_uuid_from_identifier, PASSWORD_REGEX}, - Data + utils::{PASSWORD_REGEX, generate_token, global_checks, user_uuid_from_identifier}, }; #[derive(Serialize, Deserialize)] @@ -27,8 +25,12 @@ pub struct PasswordResetToken { impl PasswordResetToken { pub async fn get(data: &Data, token: String) -> Result { - let user_uuid: Uuid = serde_json::from_str(&data.get_cache_key(format!("{}", token)).await?)?; - let password_reset_token = serde_json::from_str(&data.get_cache_key(format!("{}_password_reset", user_uuid)).await?)?; + let user_uuid: Uuid = serde_json::from_str(&data.get_cache_key(token.to_string()).await?)?; + let password_reset_token = serde_json::from_str( + &data + .get_cache_key(format!("{user_uuid}_password_reset")) + .await?, + )?; Ok(password_reset_token) } @@ -41,7 +43,11 @@ impl PasswordResetToken { let user_uuid = user_uuid_from_identifier(&mut conn, &identifier).await?; - let password_reset_token = serde_json::from_str(&data.get_cache_key(format!("{}_password_reset", user_uuid)).await?)?; + let password_reset_token = serde_json::from_str( + &data + .get_cache_key(format!("{user_uuid}_password_reset")) + .await?, + )?; Ok(password_reset_token) } @@ -69,12 +75,17 @@ impl PasswordResetToken { created_at: Utc::now(), }; - data.set_cache_key(format!("{}_password_reset", user_uuid), password_reset_token, 86400).await?; + data.set_cache_key( + format!("{user_uuid}_password_reset"), + password_reset_token, + 86400, + ) + .await?; data.set_cache_key(token.clone(), user_uuid, 86400).await?; let mut reset_endpoint = data.config.web.frontend_url.join("reset-password")?; - reset_endpoint.set_query(Some(&format!("token={}", token))); + reset_endpoint.set_query(Some(&format!("token={token}"))); let email = data .mail_client @@ -134,12 +145,13 @@ impl PasswordResetToken { data.mail_client.send_mail(email).await?; - self.delete(&data).await + self.delete(data).await } pub async fn delete(&self, data: &Data) -> Result<(), Error> { - data.del_cache_key(format!("{}_password_reset", &self.user_uuid)).await?; - data.del_cache_key(format!("{}", &self.token)).await?; + data.del_cache_key(format!("{}_password_reset", &self.user_uuid)) + .await?; + data.del_cache_key(self.token.to_string()).await?; Ok(()) } diff --git a/src/objects/role.rs b/src/objects/role.rs index a78798a..4a75628 100644 --- a/src/objects/role.rs +++ b/src/objects/role.rs @@ -6,7 +6,12 @@ use diesel_async::RunQueryDsl; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::{error::Error, schema::{role_members, roles}, utils::order_by_is_above, Conn, Data}; +use crate::{ + Conn, Data, + error::Error, + schema::{role_members, roles}, + utils::order_by_is_above, +}; use super::{HasIsAbove, HasUuid, load_or_empty}; @@ -70,8 +75,8 @@ impl Role { } pub async fn fetch_from_member(data: &Data, member_uuid: Uuid) -> Result, Error> { - if let Ok(roles) = data.get_cache_key(format!("{}_roles", member_uuid)).await { - return Ok(serde_json::from_str(&roles)?) + if let Ok(roles) = data.get_cache_key(format!("{member_uuid}_roles")).await { + return Ok(serde_json::from_str(&roles)?); } let mut conn = data.pool.get().await?; @@ -91,7 +96,8 @@ impl Role { roles.push(membership.fetch_role(&mut conn).await?); } - data.set_cache_key(format!("{}_roles", member_uuid), roles.clone(), 300).await?; + data.set_cache_key(format!("{member_uuid}_roles"), roles.clone(), 300) + .await?; Ok(roles) } @@ -108,7 +114,7 @@ impl Role { } pub async fn fetch_permissions(&self) -> Vec { - Permissions::fetch_permissions(self.permissions.clone()) + Permissions::fetch_permissions(self.permissions) } pub async fn new(conn: &mut Conn, guild_uuid: Uuid, name: String) -> Result { From b6df1e38ad99dc1b3decb213be90866528c73206 Mon Sep 17 00:00:00 2001 From: Radical Date: Wed, 2 Jul 2025 21:26:03 +0200 Subject: [PATCH 070/160] change avatar/icon lengths in users/guilds --- .../2025-07-02-192220_change_maximum_url_lengths/down.sql | 3 +++ .../2025-07-02-192220_change_maximum_url_lengths/up.sql | 3 +++ src/schema.rs | 4 ++-- 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 migrations/2025-07-02-192220_change_maximum_url_lengths/down.sql create mode 100644 migrations/2025-07-02-192220_change_maximum_url_lengths/up.sql diff --git a/migrations/2025-07-02-192220_change_maximum_url_lengths/down.sql b/migrations/2025-07-02-192220_change_maximum_url_lengths/down.sql new file mode 100644 index 0000000..69466b4 --- /dev/null +++ b/migrations/2025-07-02-192220_change_maximum_url_lengths/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE users ALTER COLUMN avatar TYPE varchar(100); +ALTER TABLE guilds ALTER COLUMN icon TYPE varchar(100); diff --git a/migrations/2025-07-02-192220_change_maximum_url_lengths/up.sql b/migrations/2025-07-02-192220_change_maximum_url_lengths/up.sql new file mode 100644 index 0000000..bcc0afe --- /dev/null +++ b/migrations/2025-07-02-192220_change_maximum_url_lengths/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE users ALTER COLUMN avatar TYPE varchar(8000); +ALTER TABLE guilds ALTER COLUMN icon TYPE varchar(8000); diff --git a/src/schema.rs b/src/schema.rs index c7a350c..cf00ca9 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -49,7 +49,7 @@ diesel::table! { name -> Varchar, #[max_length = 300] description -> Nullable, - #[max_length = 100] + #[max_length = 8000] icon -> Nullable, } } @@ -124,7 +124,7 @@ diesel::table! { email_verified -> Bool, is_deleted -> Bool, deleted_at -> Nullable, - #[max_length = 100] + #[max_length = 8000] avatar -> Nullable, #[max_length = 32] pronouns -> Nullable, From f3760af1bb766d8b0efa268c6632a7342e94b686 Mon Sep 17 00:00:00 2001 From: Radical Date: Fri, 4 Jul 2025 21:42:23 +0200 Subject: [PATCH 071/160] feat: add reply_to field to messages --- .../down.sql | 2 ++ .../2025-07-04-183201_add_replies_to_messages/up.sql | 2 ++ src/api/v1/channels/uuid/socket.rs | 11 ++++++++++- src/objects/channel.rs | 2 ++ src/objects/message.rs | 3 +++ src/schema.rs | 1 + 6 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 migrations/2025-07-04-183201_add_replies_to_messages/down.sql create mode 100644 migrations/2025-07-04-183201_add_replies_to_messages/up.sql diff --git a/migrations/2025-07-04-183201_add_replies_to_messages/down.sql b/migrations/2025-07-04-183201_add_replies_to_messages/down.sql new file mode 100644 index 0000000..bc705d7 --- /dev/null +++ b/migrations/2025-07-04-183201_add_replies_to_messages/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE messages DROP COLUMN reply_to; diff --git a/migrations/2025-07-04-183201_add_replies_to_messages/up.sql b/migrations/2025-07-04-183201_add_replies_to_messages/up.sql new file mode 100644 index 0000000..ba90379 --- /dev/null +++ b/migrations/2025-07-04-183201_add_replies_to_messages/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE messages ADD COLUMN reply_to UUID REFERENCES messages(uuid) DEFAULT NULL; diff --git a/src/api/v1/channels/uuid/socket.rs b/src/api/v1/channels/uuid/socket.rs index b346e8e..5fca5e9 100644 --- a/src/api/v1/channels/uuid/socket.rs +++ b/src/api/v1/channels/uuid/socket.rs @@ -5,6 +5,7 @@ use actix_web::{ }; use actix_ws::AggregatedMessage; use futures_util::StreamExt as _; +use serde::Deserialize; use uuid::Uuid; use crate::{ @@ -14,6 +15,12 @@ use crate::{ utils::{get_ws_protocol_header, global_checks}, }; +#[derive(Deserialize)] +struct MessageBody { + message: String, + reply_to: Option, +} + #[get("/{uuid}/socket")] pub async fn ws( req: HttpRequest, @@ -74,7 +81,9 @@ pub async fn ws( Ok(AggregatedMessage::Text(text)) => { let mut conn = data.cache_pool.get_multiplexed_tokio_connection().await?; - let message = channel.new_message(&data, uuid, text.to_string()).await?; + let message_body: MessageBody = serde_json::from_str(&text)?; + + let message = channel.new_message(&data, uuid, message_body.message, message_body.reply_to).await?; redis::cmd("PUBLISH") .arg(&[channel_uuid.to_string(), serde_json::to_string(&message)?]) diff --git a/src/objects/channel.rs b/src/objects/channel.rs index 7d21a1d..1192c69 100644 --- a/src/objects/channel.rs +++ b/src/objects/channel.rs @@ -270,6 +270,7 @@ impl Channel { data: &Data, user_uuid: Uuid, message: String, + reply_to: Option, ) -> Result { let message_uuid = Uuid::now_v7(); @@ -278,6 +279,7 @@ impl Channel { channel_uuid: self.uuid, user_uuid, message, + reply_to, }; let mut conn = data.pool.get().await?; diff --git a/src/objects/message.rs b/src/objects/message.rs index 6c1700a..a887541 100644 --- a/src/objects/message.rs +++ b/src/objects/message.rs @@ -14,6 +14,7 @@ pub struct MessageBuilder { pub channel_uuid: Uuid, pub user_uuid: Uuid, pub message: String, + pub reply_to: Option, } impl MessageBuilder { @@ -25,6 +26,7 @@ impl MessageBuilder { channel_uuid: self.channel_uuid, user_uuid: self.user_uuid, message: self.message.clone(), + reply_to: self.reply_to, user, }) } @@ -36,5 +38,6 @@ pub struct Message { channel_uuid: Uuid, user_uuid: Uuid, message: String, + reply_to: Option, user: User, } diff --git a/src/schema.rs b/src/schema.rs index cf00ca9..f860b31 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -77,6 +77,7 @@ diesel::table! { user_uuid -> Uuid, #[max_length = 4000] message -> Varchar, + reply_to -> Nullable, } } From 8febba281692a70522a3a2c4079ba771a8b489d3 Mon Sep 17 00:00:00 2001 From: Radical Date: Sat, 5 Jul 2025 02:08:56 +0200 Subject: [PATCH 072/160] fix: change logout to get request accidentally left it as a post even though it should've been a get --- src/api/v1/auth/logout.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/v1/auth/logout.rs b/src/api/v1/auth/logout.rs index 0f265bb..d5898d3 100644 --- a/src/api/v1/auth/logout.rs +++ b/src/api/v1/auth/logout.rs @@ -1,4 +1,4 @@ -use actix_web::{HttpRequest, HttpResponse, post, web}; +use actix_web::{HttpRequest, HttpResponse, get, web}; use diesel::{ExpressionMethods, delete}; use diesel_async::RunQueryDsl; @@ -17,7 +17,7 @@ use crate::{ /// 404 Refresh token is invalid /// 401 Unauthorized (no refresh token found) /// -#[post("/logout")] +#[get("/logout")] pub async fn res(req: HttpRequest, data: web::Data) -> Result { let mut refresh_token_cookie = req.cookie("refresh_token").ok_or(Error::Unauthorized( "request has no refresh token".to_string(), From b00527633aa0f4384edebb55c4f92c33eb8bb4e7 Mon Sep 17 00:00:00 2001 From: Radical Date: Sat, 5 Jul 2025 02:41:40 +0200 Subject: [PATCH 073/160] fix: return 404 when refresh token cookie is invalid --- src/api/v1/auth/logout.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/api/v1/auth/logout.rs b/src/api/v1/auth/logout.rs index d5898d3..524a644 100644 --- a/src/api/v1/auth/logout.rs +++ b/src/api/v1/auth/logout.rs @@ -13,8 +13,11 @@ use crate::{ /// requires auth: kinda, needs refresh token set but no access token is technically required /// /// ### Responses +/// /// 200 Logged out +/// /// 404 Refresh token is invalid +/// /// 401 Unauthorized (no refresh token found) /// #[get("/logout")] @@ -27,12 +30,16 @@ pub async fn res(req: HttpRequest, data: web::Data) -> Result Date: Sun, 6 Jul 2025 01:57:21 +0200 Subject: [PATCH 074/160] fix: reply with email already verified on attempted get request from account with verified email --- src/api/v1/auth/verify_email.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/api/v1/auth/verify_email.rs b/src/api/v1/auth/verify_email.rs index e596500..e75209b 100644 --- a/src/api/v1/auth/verify_email.rs +++ b/src/api/v1/auth/verify_email.rs @@ -26,8 +26,13 @@ struct Query { /// /// ### Responses /// 200 Success +/// +/// 204 Already verified +/// /// 410 Token Expired +/// /// 404 Not Found +/// /// 401 Unauthorized /// #[get("/verify-email")] @@ -46,6 +51,10 @@ pub async fn get( let me = Me::get(&mut conn, uuid).await?; + if me.email_verified { + return Ok(HttpResponse::NoContent().finish()); + } + let email_token = EmailToken::get(&data, me.uuid).await?; if query.token != email_token.token { @@ -65,8 +74,11 @@ pub async fn get( /// /// ### Responses /// 200 Email sent +/// /// 204 Already verified +/// /// 429 Too Many Requests +/// /// 401 Unauthorized /// #[post("/verify-email")] From 4b9336dcd9f344fc3837718f7bc7097742623092 Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 6 Jul 2025 17:54:22 +0200 Subject: [PATCH 075/160] set minimum username length to 3 when changing your username --- src/objects/me.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/objects/me.rs b/src/objects/me.rs index d99a7b4..92797ee 100644 --- a/src/objects/me.rs +++ b/src/objects/me.rs @@ -124,7 +124,7 @@ impl Me { } pub async fn set_username(&mut self, data: &Data, new_username: String) -> Result<(), Error> { - if !USERNAME_REGEX.is_match(&new_username) { + if !USERNAME_REGEX.is_match(&new_username) || new_username.len() < 3 || new_username.len() > 32 { return Err(Error::BadRequest("Invalid username".to_string())); } From ac3e7e242b049b7402534e292c1c081701029211 Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 6 Jul 2025 18:06:06 +0200 Subject: [PATCH 076/160] fix: set empty display names to null automatically --- src/objects/me.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/objects/me.rs b/src/objects/me.rs index 92797ee..e322832 100644 --- a/src/objects/me.rs +++ b/src/objects/me.rs @@ -153,10 +153,18 @@ impl Me { ) -> Result<(), Error> { let mut conn = data.pool.get().await?; + let new_display_name_option; + + if new_display_name.is_empty() { + new_display_name_option = None; + } else { + new_display_name_option = Some(new_display_name) + } + use users::dsl; update(users::table) .filter(dsl::uuid.eq(self.uuid)) - .set(dsl::display_name.eq(new_display_name.as_str())) + .set(dsl::display_name.eq(&new_display_name_option)) .execute(&mut conn) .await?; @@ -164,7 +172,7 @@ impl Me { data.del_cache_key(self.uuid.to_string()).await? } - self.display_name = Some(new_display_name); + self.display_name = new_display_name_option; Ok(()) } From e8b8b4964311827e2a4da9cc81d020382daa3008 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 10 Jul 2025 15:37:38 +0200 Subject: [PATCH 077/160] feat: add friends! --- .../2025-07-07-131320_add_friends/down.sql | 4 + .../2025-07-07-131320_add_friends/up.sql | 35 ++++ src/api/v1/guilds/uuid/members.rs | 10 +- src/api/v1/me/friends/mod.rs | 76 +++++++ src/api/v1/me/friends/uuid.rs | 29 +++ src/api/v1/me/mod.rs | 4 + src/api/v1/users/uuid.rs | 10 +- src/objects/friends.rs | 24 +++ src/objects/me.rs | 188 +++++++++++++++++- src/objects/member.rs | 25 ++- src/objects/mod.rs | 3 + src/objects/user.rs | 52 ++++- src/schema.rs | 18 ++ 13 files changed, 439 insertions(+), 39 deletions(-) create mode 100644 migrations/2025-07-07-131320_add_friends/down.sql create mode 100644 migrations/2025-07-07-131320_add_friends/up.sql create mode 100644 src/api/v1/me/friends/uuid.rs create mode 100644 src/objects/friends.rs diff --git a/migrations/2025-07-07-131320_add_friends/down.sql b/migrations/2025-07-07-131320_add_friends/down.sql new file mode 100644 index 0000000..30637b7 --- /dev/null +++ b/migrations/2025-07-07-131320_add_friends/down.sql @@ -0,0 +1,4 @@ +-- This file should undo anything in `up.sql` +DROP TABLE friend_requests; +DROP FUNCTION check_friend_request; +DROP TABLE friends; diff --git a/migrations/2025-07-07-131320_add_friends/up.sql b/migrations/2025-07-07-131320_add_friends/up.sql new file mode 100644 index 0000000..2ed45ab --- /dev/null +++ b/migrations/2025-07-07-131320_add_friends/up.sql @@ -0,0 +1,35 @@ +-- Your SQL goes here +CREATE TABLE friends ( + uuid1 UUID REFERENCES users(uuid), + uuid2 UUID REFERENCES users(uuid), + accepted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (uuid1, uuid2), + CHECK (uuid1 < uuid2) +); + +CREATE TABLE friend_requests ( + sender UUID REFERENCES users(uuid), + receiver UUID REFERENCES users(uuid), + requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (sender, receiver), + CHECK (sender <> receiver) +); + +-- Create a function to check for existing friendships +CREATE FUNCTION check_friend_request() +RETURNS TRIGGER AS $$ +BEGIN + IF EXISTS ( + SELECT 1 FROM friends + WHERE (uuid1, uuid2) = (LEAST(NEW.sender, NEW.receiver), GREATEST(NEW.sender, NEW.receiver)) + ) THEN + RAISE EXCEPTION 'Users are already friends'; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create the trigger +CREATE TRIGGER prevent_friend_request_conflict +BEFORE INSERT OR UPDATE ON friend_requests +FOR EACH ROW EXECUTE FUNCTION check_friend_request(); diff --git a/src/api/v1/guilds/uuid/members.rs b/src/api/v1/guilds/uuid/members.rs index 972d862..5e7da58 100644 --- a/src/api/v1/guilds/uuid/members.rs +++ b/src/api/v1/guilds/uuid/members.rs @@ -1,9 +1,5 @@ use crate::{ - Data, - api::v1::auth::check_access_token, - error::Error, - objects::Member, - utils::{get_auth_header, global_checks}, + api::v1::auth::check_access_token, error::Error, objects::{Me, Member}, utils::{get_auth_header, global_checks}, Data }; use ::uuid::Uuid; use actix_web::{HttpRequest, HttpResponse, get, web}; @@ -28,7 +24,9 @@ pub async fn get( Member::check_membership(&mut conn, uuid, guild_uuid).await?; - let members = Member::fetch_all(&data, guild_uuid).await?; + let me = Me::get(&mut conn, uuid).await?; + + let members = Member::fetch_all(&data, &me, guild_uuid).await?; Ok(HttpResponse::Ok().json(members)) } diff --git a/src/api/v1/me/friends/mod.rs b/src/api/v1/me/friends/mod.rs index e69de29..77e1a64 100644 --- a/src/api/v1/me/friends/mod.rs +++ b/src/api/v1/me/friends/mod.rs @@ -0,0 +1,76 @@ +use actix_web::{HttpRequest, HttpResponse, get, post, web}; +use serde::Deserialize; +use ::uuid::Uuid; + +pub mod uuid; + +use crate::{ + Data, + api::v1::auth::check_access_token, + error::Error, + objects::Me, + utils::{get_auth_header, global_checks}, +}; + +/// Returns a list of users that are your friends +#[get("/friends")] +pub async fn get(req: HttpRequest, data: web::Data) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + global_checks(&data, uuid).await?; + + let me = Me::get(&mut conn, uuid).await?; + + let friends = me.get_friends(&data).await?; + + Ok(HttpResponse::Ok().json(friends)) +} + +#[derive(Deserialize)] +struct UserReq { + uuid: Uuid, +} + +/// `POST /api/v1/me/friends` Send friend request +/// +/// requires auth? yes +/// +/// ### Request Example: +/// ``` +/// json!({ +/// "uuid": "155d2291-fb23-46bd-a656-ae7c5d8218e6", +/// }); +/// ``` +/// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps +/// +/// ### Responses +/// 200 Success +/// +/// 404 Not Found +/// +/// 400 Bad Request (usually means users are already friends) +/// +#[post("/friends")] +pub async fn post(req: HttpRequest, json: web::Json, data: web::Data) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + global_checks(&data, uuid).await?; + + let me = Me::get(&mut conn, uuid).await?; + + me.add_friend(&mut conn, json.uuid).await?; + + Ok(HttpResponse::Ok().finish()) +} diff --git a/src/api/v1/me/friends/uuid.rs b/src/api/v1/me/friends/uuid.rs new file mode 100644 index 0000000..142dbc3 --- /dev/null +++ b/src/api/v1/me/friends/uuid.rs @@ -0,0 +1,29 @@ +use actix_web::{HttpRequest, HttpResponse, delete, web}; +use uuid::Uuid; + +use crate::{ + Data, + api::v1::auth::check_access_token, + error::Error, + objects::Me, + utils::{get_auth_header, global_checks}, +}; + +#[delete("/friends/{uuid}")] +pub async fn delete(req: HttpRequest, path: web::Path<(Uuid,)>, data: web::Data) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + global_checks(&data, uuid).await?; + + let me = Me::get(&mut conn, uuid).await?; + + me.remove_friend(&mut conn, path.0).await?; + + Ok(HttpResponse::Ok().finish()) +} diff --git a/src/api/v1/me/mod.rs b/src/api/v1/me/mod.rs index da5c929..dfa5fdc 100644 --- a/src/api/v1/me/mod.rs +++ b/src/api/v1/me/mod.rs @@ -11,12 +11,16 @@ use crate::{ }; mod guilds; +mod friends; pub fn web() -> Scope { web::scope("/me") .service(get) .service(update) .service(guilds::get) + .service(friends::get) + .service(friends::post) + .service(friends::uuid::delete) } #[get("")] diff --git a/src/api/v1/users/uuid.rs b/src/api/v1/users/uuid.rs index 9e602a0..cd16c31 100644 --- a/src/api/v1/users/uuid.rs +++ b/src/api/v1/users/uuid.rs @@ -4,11 +4,7 @@ use actix_web::{HttpRequest, HttpResponse, get, web}; use uuid::Uuid; use crate::{ - Data, - api::v1::auth::check_access_token, - error::Error, - objects::User, - utils::{get_auth_header, global_checks}, + api::v1::auth::check_access_token, error::Error, objects::{Me, User}, utils::{get_auth_header, global_checks}, Data }; /// `GET /api/v1/users/{uuid}` Returns user with the given UUID @@ -45,7 +41,9 @@ pub async fn get( global_checks(&data, uuid).await?; - let user = User::fetch_one(&data, user_uuid).await?; + let me = Me::get(&mut conn, uuid).await?; + + let user = User::fetch_one_with_friendship(&data, &me, user_uuid).await?; Ok(HttpResponse::Ok().json(user)) } diff --git a/src/objects/friends.rs b/src/objects/friends.rs new file mode 100644 index 0000000..a86eb2b --- /dev/null +++ b/src/objects/friends.rs @@ -0,0 +1,24 @@ +use chrono::{DateTime, Utc}; +use diesel::{Queryable, Selectable}; +use serde::Serialize; +use uuid::Uuid; + +use crate::schema::{friend_requests, friends}; + +#[derive(Serialize, Queryable, Selectable, Clone)] +#[diesel(table_name = friends)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct Friend { + pub uuid1: Uuid, + pub uuid2: Uuid, + pub accepted_at: DateTime, +} + +#[derive(Serialize, Queryable, Selectable, Clone)] +#[diesel(table_name = friend_requests)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct FriendRequest { + pub sender: Uuid, + pub receiver: Uuid, + pub requested_at: DateTime, +} diff --git a/src/objects/me.rs b/src/objects/me.rs index e322832..3e4e20e 100644 --- a/src/objects/me.rs +++ b/src/objects/me.rs @@ -1,5 +1,5 @@ use actix_web::web::BytesMut; -use diesel::{ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper, update}; +use diesel::{delete, insert_into, update, ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper}; use diesel_async::RunQueryDsl; use serde::Serialize; use tokio::task; @@ -7,10 +7,7 @@ use url::Url; use uuid::Uuid; use crate::{ - Conn, Data, - error::Error, - schema::{guild_members, guilds, users}, - utils::{EMAIL_REGEX, USERNAME_REGEX, image_check}, + error::Error, objects::{FriendRequest, Friend, User}, schema::{friend_requests, friends, guild_members, guilds, users}, utils::{image_check, EMAIL_REGEX, USERNAME_REGEX}, Conn, Data }; use super::{Guild, guild::GuildBuilder, load_or_empty, member::MemberBuilder}; @@ -153,13 +150,11 @@ impl Me { ) -> Result<(), Error> { let mut conn = data.pool.get().await?; - let new_display_name_option; - - if new_display_name.is_empty() { - new_display_name_option = None; + let new_display_name_option = if new_display_name.is_empty() { + None } else { - new_display_name_option = Some(new_display_name) - } + Some(new_display_name) + }; use users::dsl; update(users::table) @@ -236,4 +231,175 @@ impl Me { Ok(()) } + + pub async fn friends_with(&self, conn: &mut Conn, user_uuid: Uuid) -> Result, Error> { + use friends::dsl; + + let friends: Vec = if self.uuid < user_uuid { + load_or_empty( + dsl::friends + .filter(dsl::uuid1.eq(self.uuid)) + .filter(dsl::uuid2.eq(user_uuid)) + .load(conn) + .await + )? + } else { + load_or_empty( + dsl::friends + .filter(dsl::uuid1.eq(user_uuid)) + .filter(dsl::uuid2.eq(self.uuid)) + .load(conn) + .await + )? + }; + + if friends.is_empty() { + return Ok(None) + } + + Ok(Some(friends[0].clone())) + } + + pub async fn add_friend(&self, conn: &mut Conn, user_uuid: Uuid) -> Result<(), Error> { + if self.friends_with(conn, user_uuid).await?.is_some() { + // TODO: Check if another error should be used + return Err(Error::BadRequest("Already friends with user".to_string())) + } + + use friend_requests::dsl; + + let friend_request: Vec = load_or_empty( + dsl::friend_requests + .filter(dsl::sender.eq(user_uuid)) + .filter(dsl::receiver.eq(self.uuid)) + .load(conn) + .await + )?; + + #[allow(clippy::get_first)] + if let Some(friend_request) = friend_request.get(0) { + use friends::dsl; + + if self.uuid < user_uuid { + insert_into(friends::table) + .values((dsl::uuid1.eq(self.uuid), dsl::uuid2.eq(user_uuid))) + .execute(conn) + .await?; + } else { + insert_into(friends::table) + .values((dsl::uuid1.eq(user_uuid), dsl::uuid2.eq(self.uuid))) + .execute(conn) + .await?; + } + + use friend_requests::dsl as frdsl; + + delete(friend_requests::table) + .filter(frdsl::sender.eq(friend_request.sender)) + .filter(frdsl::receiver.eq(friend_request.receiver)) + .execute(conn) + .await?; + + Ok(()) + } else { + use friend_requests::dsl; + + insert_into(friend_requests::table) + .values((dsl::sender.eq(self.uuid), dsl::receiver.eq(user_uuid))) + .execute(conn) + .await?; + + Ok(()) + } + } + + pub async fn remove_friend(&self, conn: &mut Conn, user_uuid: Uuid) -> Result<(), Error> { + if self.friends_with(conn, user_uuid).await?.is_none() { + // TODO: Check if another error should be used + return Err(Error::BadRequest("Not friends with user".to_string())) + } + + use friends::dsl; + + if self.uuid < user_uuid { + delete(friends::table) + .filter(dsl::uuid1.eq(self.uuid)) + .filter(dsl::uuid2.eq(user_uuid)) + .execute(conn) + .await?; + } else { + delete(friends::table) + .filter(dsl::uuid1.eq(user_uuid)) + .filter(dsl::uuid2.eq(self.uuid)) + .execute(conn) + .await?; + } + + Ok(()) + } + + pub async fn get_friends(&self, data: &Data) -> Result, Error> { + use friends::dsl; + + let mut conn = data.pool.get().await?; + + let friends1 = load_or_empty( + dsl::friends + .filter(dsl::uuid1.eq(self.uuid)) + .select(Friend::as_select()) + .load(&mut conn) + .await + )?; + + let friends2 = load_or_empty( + dsl::friends + .filter(dsl::uuid2.eq(self.uuid)) + .select(Friend::as_select()) + .load(&mut conn) + .await + )?; + + let friend_futures = friends1.iter().map(async move |friend| { + User::fetch_one_with_friendship(data, self, friend.uuid2).await + }); + + let mut friends = futures::future::try_join_all(friend_futures).await?; + + let friend_futures = friends2.iter().map(async move |friend| { + User::fetch_one_with_friendship(data, self, friend.uuid1).await + }); + + friends.append(&mut futures::future::try_join_all(friend_futures).await?); + + Ok(friends) + } + + /* TODO + pub async fn get_friend_requests(&self, conn: &mut Conn) -> Result, Error> { + use friend_requests::dsl; + + let friend_request: Vec = load_or_empty( + dsl::friend_requests + .filter(dsl::receiver.eq(self.uuid)) + .load(conn) + .await + )?; + + Ok() + } + + pub async fn delete_friend_request(&self, conn: &mut Conn, user_uuid: Uuid) -> Result, Error> { + use friend_requests::dsl; + + let friend_request: Vec = load_or_empty( + dsl::friend_requests + .filter(dsl::sender.eq(user_uuid)) + .filter(dsl::receiver.eq(self.uuid)) + .load(conn) + .await + )?; + + Ok() + } + */ } diff --git a/src/objects/member.rs b/src/objects/member.rs index d33d2b6..d11126c 100644 --- a/src/objects/member.rs +++ b/src/objects/member.rs @@ -6,10 +6,7 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ - Conn, Data, - error::Error, - objects::{Permissions, Role}, - schema::guild_members, + error::Error, objects::{Me, Permissions, Role}, schema::guild_members, Conn, Data }; use super::{User, load_or_empty}; @@ -26,8 +23,14 @@ pub struct MemberBuilder { } impl MemberBuilder { - pub async fn build(&self, data: &Data) -> Result { - let user = User::fetch_one(data, self.user_uuid).await?; + pub async fn build(&self, data: &Data, me: Option<&Me>) -> Result { + let user; + + if let Some(me) = me { + user = User::fetch_one_with_friendship(data, me, self.user_uuid).await?; + } else { + user = User::fetch_one(data, self.user_uuid).await?; + } Ok(Member { uuid: self.uuid, @@ -94,7 +97,7 @@ impl Member { Ok(member_builder) } - pub async fn fetch_one(data: &Data, user_uuid: Uuid, guild_uuid: Uuid) -> Result { + pub async fn fetch_one(data: &Data, me: &Me, user_uuid: Uuid, guild_uuid: Uuid) -> Result { let mut conn = data.pool.get().await?; use guild_members::dsl; @@ -105,10 +108,10 @@ impl Member { .get_result(&mut conn) .await?; - member.build(data).await + member.build(data, Some(me)).await } - pub async fn fetch_all(data: &Data, guild_uuid: Uuid) -> Result, Error> { + pub async fn fetch_all(data: &Data, me: &Me, guild_uuid: Uuid) -> Result, Error> { let mut conn = data.pool.get().await?; use guild_members::dsl; @@ -122,7 +125,7 @@ impl Member { let member_futures = member_builders .iter() - .map(async move |m| m.build(data).await); + .map(async move |m| m.build(data, Some(me)).await); futures::future::try_join_all(member_futures).await } @@ -145,6 +148,6 @@ impl Member { .execute(&mut conn) .await?; - member.build(data).await + member.build(data, None).await } } diff --git a/src/objects/mod.rs b/src/objects/mod.rs index d8de266..2ca58e6 100644 --- a/src/objects/mod.rs +++ b/src/objects/mod.rs @@ -17,6 +17,7 @@ mod message; mod password_reset_token; mod role; mod user; +mod friends; pub use channel::Channel; pub use email_token::EmailToken; @@ -29,6 +30,8 @@ pub use password_reset_token::PasswordResetToken; pub use role::Permissions; pub use role::Role; pub use user::User; +pub use friends::Friend; +pub use friends::FriendRequest; use crate::error::Error; diff --git a/src/objects/user.rs b/src/objects/user.rs index 98e5e80..eb28694 100644 --- a/src/objects/user.rs +++ b/src/objects/user.rs @@ -1,15 +1,40 @@ +use chrono::{DateTime, Utc}; use diesel::{ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper}; use diesel_async::RunQueryDsl; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::{Conn, Data, error::Error, schema::users}; +use crate::{error::Error, objects::Me, schema::users, Conn, Data}; use super::load_or_empty; #[derive(Deserialize, Serialize, Clone, Queryable, Selectable)] #[diesel(table_name = users)] #[diesel(check_for_backend(diesel::pg::Pg))] +pub struct UserBuilder { + uuid: Uuid, + username: String, + display_name: Option, + avatar: Option, + pronouns: Option, + about: Option, +} + +impl UserBuilder { + fn build(self) -> User { + User { + uuid: self.uuid, + username: self.username, + display_name: self.display_name, + avatar: self.avatar, + pronouns: self.pronouns, + about: self.about, + friends_since: None, + } + } +} + +#[derive(Deserialize, Serialize, Clone)] pub struct User { uuid: Uuid, username: String, @@ -17,6 +42,7 @@ pub struct User { avatar: Option, pronouns: Option, about: Option, + friends_since: Option>, } impl User { @@ -28,33 +54,49 @@ impl User { } use users::dsl; - let user: User = dsl::users + let user_builder: UserBuilder = dsl::users .filter(dsl::uuid.eq(user_uuid)) - .select(User::as_select()) + .select(UserBuilder::as_select()) .get_result(&mut conn) .await?; + let user = user_builder.build(); + data.set_cache_key(user_uuid.to_string(), user.clone(), 1800) .await?; Ok(user) } + pub async fn fetch_one_with_friendship(data: &Data, me: &Me, user_uuid: Uuid) -> Result { + let mut conn = data.pool.get().await?; + + let mut user = Self::fetch_one(data, user_uuid).await?; + + if let Some(friend) = me.friends_with(&mut conn, user_uuid).await? { + user.friends_since = Some(friend.accepted_at); + } + + Ok(user) + } + pub async fn fetch_amount( conn: &mut Conn, offset: i64, amount: i64, ) -> Result, Error> { use users::dsl; - let users: Vec = load_or_empty( + let user_builders: Vec = load_or_empty( dsl::users .limit(amount) .offset(offset) - .select(User::as_select()) + .select(UserBuilder::as_select()) .load(conn) .await, )?; + let users: Vec = user_builders.iter().map(|u| u.clone().build()).collect(); + Ok(users) } } diff --git a/src/schema.rs b/src/schema.rs index f860b31..2693b02 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -31,6 +31,22 @@ diesel::table! { } } +diesel::table! { + friend_requests (sender, receiver) { + sender -> Uuid, + receiver -> Uuid, + requested_at -> Timestamptz, + } +} + +diesel::table! { + friends (uuid1, uuid2) { + uuid1 -> Uuid, + uuid2 -> Uuid, + accepted_at -> Timestamptz, + } +} + diesel::table! { guild_members (uuid) { uuid -> Uuid, @@ -153,6 +169,8 @@ diesel::allow_tables_to_appear_in_same_query!( access_tokens, channel_permissions, channels, + friend_requests, + friends, guild_members, guilds, instance_permissions, From e8a9857e19a9d6b9a09ea41279a33cb1d47754d8 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 10 Jul 2025 15:37:45 +0200 Subject: [PATCH 078/160] style: cargo fmt --- src/api/v1/auth/logout.rs | 10 +++++--- src/api/v1/auth/verify_email.rs | 14 +++++------ src/api/v1/channels/uuid/socket.rs | 4 ++- src/api/v1/guilds/uuid/members.rs | 6 ++++- src/api/v1/me/friends/mod.rs | 12 ++++++--- src/api/v1/me/friends/uuid.rs | 6 ++++- src/api/v1/me/mod.rs | 2 +- src/api/v1/users/uuid.rs | 6 ++++- src/objects/friends.rs | 2 +- src/objects/me.rs | 40 ++++++++++++++++++++---------- src/objects/member.rs | 12 +++++++-- src/objects/mod.rs | 6 ++--- src/objects/user.rs | 8 ++++-- 13 files changed, 87 insertions(+), 41 deletions(-) diff --git a/src/api/v1/auth/logout.rs b/src/api/v1/auth/logout.rs index 524a644..b805d91 100644 --- a/src/api/v1/auth/logout.rs +++ b/src/api/v1/auth/logout.rs @@ -13,11 +13,11 @@ use crate::{ /// requires auth: kinda, needs refresh token set but no access token is technically required /// /// ### Responses -/// +/// /// 200 Logged out -/// +/// /// 404 Refresh token is invalid -/// +/// /// 401 Unauthorized (no refresh token found) /// #[get("/logout")] @@ -38,7 +38,9 @@ pub async fn res(req: HttpRequest, data: web::Data) -> Result, data: web::Data) -> Result { +pub async fn post( + req: HttpRequest, + json: web::Json, + data: web::Data, +) -> Result { let headers = req.headers(); let auth_header = get_auth_header(headers)?; diff --git a/src/api/v1/me/friends/uuid.rs b/src/api/v1/me/friends/uuid.rs index 142dbc3..34bfeff 100644 --- a/src/api/v1/me/friends/uuid.rs +++ b/src/api/v1/me/friends/uuid.rs @@ -10,7 +10,11 @@ use crate::{ }; #[delete("/friends/{uuid}")] -pub async fn delete(req: HttpRequest, path: web::Path<(Uuid,)>, data: web::Data) -> Result { +pub async fn delete( + req: HttpRequest, + path: web::Path<(Uuid,)>, + data: web::Data, +) -> Result { let headers = req.headers(); let auth_header = get_auth_header(headers)?; diff --git a/src/api/v1/me/mod.rs b/src/api/v1/me/mod.rs index dfa5fdc..f667ca4 100644 --- a/src/api/v1/me/mod.rs +++ b/src/api/v1/me/mod.rs @@ -10,8 +10,8 @@ use crate::{ utils::{get_auth_header, global_checks}, }; -mod guilds; mod friends; +mod guilds; pub fn web() -> Scope { web::scope("/me") diff --git a/src/api/v1/users/uuid.rs b/src/api/v1/users/uuid.rs index cd16c31..5d36b75 100644 --- a/src/api/v1/users/uuid.rs +++ b/src/api/v1/users/uuid.rs @@ -4,7 +4,11 @@ use actix_web::{HttpRequest, HttpResponse, get, web}; use uuid::Uuid; use crate::{ - api::v1::auth::check_access_token, error::Error, objects::{Me, User}, utils::{get_auth_header, global_checks}, Data + Data, + api::v1::auth::check_access_token, + error::Error, + objects::{Me, User}, + utils::{get_auth_header, global_checks}, }; /// `GET /api/v1/users/{uuid}` Returns user with the given UUID diff --git a/src/objects/friends.rs b/src/objects/friends.rs index a86eb2b..9d23512 100644 --- a/src/objects/friends.rs +++ b/src/objects/friends.rs @@ -9,7 +9,7 @@ use crate::schema::{friend_requests, friends}; #[diesel(table_name = friends)] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct Friend { - pub uuid1: Uuid, + pub uuid1: Uuid, pub uuid2: Uuid, pub accepted_at: DateTime, } diff --git a/src/objects/me.rs b/src/objects/me.rs index 3e4e20e..37951ab 100644 --- a/src/objects/me.rs +++ b/src/objects/me.rs @@ -1,5 +1,8 @@ use actix_web::web::BytesMut; -use diesel::{delete, insert_into, update, ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper}; +use diesel::{ + ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper, delete, insert_into, + update, +}; use diesel_async::RunQueryDsl; use serde::Serialize; use tokio::task; @@ -7,7 +10,11 @@ use url::Url; use uuid::Uuid; use crate::{ - error::Error, objects::{FriendRequest, Friend, User}, schema::{friend_requests, friends, guild_members, guilds, users}, utils::{image_check, EMAIL_REGEX, USERNAME_REGEX}, Conn, Data + Conn, Data, + error::Error, + objects::{Friend, FriendRequest, User}, + schema::{friend_requests, friends, guild_members, guilds, users}, + utils::{EMAIL_REGEX, USERNAME_REGEX, image_check}, }; use super::{Guild, guild::GuildBuilder, load_or_empty, member::MemberBuilder}; @@ -121,7 +128,10 @@ impl Me { } pub async fn set_username(&mut self, data: &Data, new_username: String) -> Result<(), Error> { - if !USERNAME_REGEX.is_match(&new_username) || new_username.len() < 3 || new_username.len() > 32 { + if !USERNAME_REGEX.is_match(&new_username) + || new_username.len() < 3 + || new_username.len() > 32 + { return Err(Error::BadRequest("Invalid username".to_string())); } @@ -232,7 +242,11 @@ impl Me { Ok(()) } - pub async fn friends_with(&self, conn: &mut Conn, user_uuid: Uuid) -> Result, Error> { + pub async fn friends_with( + &self, + conn: &mut Conn, + user_uuid: Uuid, + ) -> Result, Error> { use friends::dsl; let friends: Vec = if self.uuid < user_uuid { @@ -241,7 +255,7 @@ impl Me { .filter(dsl::uuid1.eq(self.uuid)) .filter(dsl::uuid2.eq(user_uuid)) .load(conn) - .await + .await, )? } else { load_or_empty( @@ -249,12 +263,12 @@ impl Me { .filter(dsl::uuid1.eq(user_uuid)) .filter(dsl::uuid2.eq(self.uuid)) .load(conn) - .await + .await, )? }; if friends.is_empty() { - return Ok(None) + return Ok(None); } Ok(Some(friends[0].clone())) @@ -263,9 +277,9 @@ impl Me { pub async fn add_friend(&self, conn: &mut Conn, user_uuid: Uuid) -> Result<(), Error> { if self.friends_with(conn, user_uuid).await?.is_some() { // TODO: Check if another error should be used - return Err(Error::BadRequest("Already friends with user".to_string())) + return Err(Error::BadRequest("Already friends with user".to_string())); } - + use friend_requests::dsl; let friend_request: Vec = load_or_empty( @@ -273,7 +287,7 @@ impl Me { .filter(dsl::sender.eq(user_uuid)) .filter(dsl::receiver.eq(self.uuid)) .load(conn) - .await + .await, )?; #[allow(clippy::get_first)] @@ -316,7 +330,7 @@ impl Me { pub async fn remove_friend(&self, conn: &mut Conn, user_uuid: Uuid) -> Result<(), Error> { if self.friends_with(conn, user_uuid).await?.is_none() { // TODO: Check if another error should be used - return Err(Error::BadRequest("Not friends with user".to_string())) + return Err(Error::BadRequest("Not friends with user".to_string())); } use friends::dsl; @@ -348,7 +362,7 @@ impl Me { .filter(dsl::uuid1.eq(self.uuid)) .select(Friend::as_select()) .load(&mut conn) - .await + .await, )?; let friends2 = load_or_empty( @@ -356,7 +370,7 @@ impl Me { .filter(dsl::uuid2.eq(self.uuid)) .select(Friend::as_select()) .load(&mut conn) - .await + .await, )?; let friend_futures = friends1.iter().map(async move |friend| { diff --git a/src/objects/member.rs b/src/objects/member.rs index d11126c..361e4b3 100644 --- a/src/objects/member.rs +++ b/src/objects/member.rs @@ -6,7 +6,10 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ - error::Error, objects::{Me, Permissions, Role}, schema::guild_members, Conn, Data + Conn, Data, + error::Error, + objects::{Me, Permissions, Role}, + schema::guild_members, }; use super::{User, load_or_empty}; @@ -97,7 +100,12 @@ impl Member { Ok(member_builder) } - pub async fn fetch_one(data: &Data, me: &Me, user_uuid: Uuid, guild_uuid: Uuid) -> Result { + pub async fn fetch_one( + data: &Data, + me: &Me, + user_uuid: Uuid, + guild_uuid: Uuid, + ) -> Result { let mut conn = data.pool.get().await?; use guild_members::dsl; diff --git a/src/objects/mod.rs b/src/objects/mod.rs index 2ca58e6..9974410 100644 --- a/src/objects/mod.rs +++ b/src/objects/mod.rs @@ -9,6 +9,7 @@ use uuid::Uuid; mod channel; mod email_token; +mod friends; mod guild; mod invite; mod me; @@ -17,10 +18,11 @@ mod message; mod password_reset_token; mod role; mod user; -mod friends; pub use channel::Channel; pub use email_token::EmailToken; +pub use friends::Friend; +pub use friends::FriendRequest; pub use guild::Guild; pub use invite::Invite; pub use me::Me; @@ -30,8 +32,6 @@ pub use password_reset_token::PasswordResetToken; pub use role::Permissions; pub use role::Role; pub use user::User; -pub use friends::Friend; -pub use friends::FriendRequest; use crate::error::Error; diff --git a/src/objects/user.rs b/src/objects/user.rs index eb28694..8e42351 100644 --- a/src/objects/user.rs +++ b/src/objects/user.rs @@ -4,7 +4,7 @@ use diesel_async::RunQueryDsl; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::{error::Error, objects::Me, schema::users, Conn, Data}; +use crate::{Conn, Data, error::Error, objects::Me, schema::users}; use super::load_or_empty; @@ -68,7 +68,11 @@ impl User { Ok(user) } - pub async fn fetch_one_with_friendship(data: &Data, me: &Me, user_uuid: Uuid) -> Result { + pub async fn fetch_one_with_friendship( + data: &Data, + me: &Me, + user_uuid: Uuid, + ) -> Result { let mut conn = data.pool.get().await?; let mut user = Self::fetch_one(data, user_uuid).await?; From 43c457779705be751a7fcee9b5267dfdc51e1a61 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 10 Jul 2025 20:57:48 +0200 Subject: [PATCH 079/160] fix: increase max size and set timeouts on pool --- src/main.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 248289a..d0e22f9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ use diesel_async::pooled_connection::deadpool::Pool; use error::Error; use objects::MailClient; use simple_logger::SimpleLogger; -use std::time::SystemTime; +use std::time::{Duration, SystemTime}; mod config; use config::{Config, ConfigBuilder}; use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations}; @@ -61,7 +61,12 @@ async fn main() -> Result<(), Error> { // create a new connection pool with the default config let pool_config = AsyncDieselConnectionManager::::new(config.database.url()); - let pool = Pool::builder(pool_config).build()?; + // FIXME: Don't manually set max size and instead fix underlying connection issues + let pool = Pool::builder(pool_config) + .max_size(50) + .wait_timeout(Some(Duration::from_secs(5))) + .recycle_timeout(Some(Duration::from_secs(5))) + .build()?; let cache_pool = redis::Client::open(config.cache_database.url())?; From 2013befda26235e74ff8576fd20d138bc7b36bd0 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 10 Jul 2025 21:31:43 +0200 Subject: [PATCH 080/160] fix: try not setting timeouts --- src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index d0e22f9..12c1d81 100644 --- a/src/main.rs +++ b/src/main.rs @@ -64,8 +64,8 @@ async fn main() -> Result<(), Error> { // FIXME: Don't manually set max size and instead fix underlying connection issues let pool = Pool::builder(pool_config) .max_size(50) - .wait_timeout(Some(Duration::from_secs(5))) - .recycle_timeout(Some(Duration::from_secs(5))) + //.wait_timeout(Some(Duration::from_secs(5))) + //.recycle_timeout(Some(Duration::from_secs(5))) .build()?; let cache_pool = redis::Client::open(config.cache_database.url())?; From 1a0fefd364f0b2412addac8bea346c04dade7392 Mon Sep 17 00:00:00 2001 From: Radical Date: Fri, 11 Jul 2025 03:06:47 +0200 Subject: [PATCH 081/160] fix: resolve issues with max connections to db This might need tweaking elsewhere, needs more testing to figure out where faults are happening --- src/main.rs | 9 ++------- src/objects/member.rs | 10 ++++++---- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/main.rs b/src/main.rs index 12c1d81..248289a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ use diesel_async::pooled_connection::deadpool::Pool; use error::Error; use objects::MailClient; use simple_logger::SimpleLogger; -use std::time::{Duration, SystemTime}; +use std::time::SystemTime; mod config; use config::{Config, ConfigBuilder}; use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations}; @@ -61,12 +61,7 @@ async fn main() -> Result<(), Error> { // create a new connection pool with the default config let pool_config = AsyncDieselConnectionManager::::new(config.database.url()); - // FIXME: Don't manually set max size and instead fix underlying connection issues - let pool = Pool::builder(pool_config) - .max_size(50) - //.wait_timeout(Some(Duration::from_secs(5))) - //.recycle_timeout(Some(Duration::from_secs(5))) - .build()?; + let pool = Pool::builder(pool_config).build()?; let cache_pool = redis::Client::open(config.cache_database.url())?; diff --git a/src/objects/member.rs b/src/objects/member.rs index 361e4b3..c2a71d9 100644 --- a/src/objects/member.rs +++ b/src/objects/member.rs @@ -131,11 +131,13 @@ impl Member { .await, )?; - let member_futures = member_builders - .iter() - .map(async move |m| m.build(data, Some(me)).await); + let mut members = vec![]; - futures::future::try_join_all(member_futures).await + for builder in member_builders { + members.push(builder.build(&data, Some(me)).await?); + } + + Ok(members) } pub async fn new(data: &Data, user_uuid: Uuid, guild_uuid: Uuid) -> Result { From 42b2d08a0019093b93c48069a9c9c5025a2f17e4 Mon Sep 17 00:00:00 2001 From: Radical Date: Sat, 12 Jul 2025 16:15:30 +0200 Subject: [PATCH 082/160] feat: make permissions more concise --- src/api/v1/guilds/uuid/channels.rs | 2 +- src/api/v1/guilds/uuid/icon.rs | 2 +- src/api/v1/guilds/uuid/roles/mod.rs | 2 +- src/objects/role.rs | 29 ++++++++++++++--------------- 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/api/v1/guilds/uuid/channels.rs b/src/api/v1/guilds/uuid/channels.rs index 8fda917..b9f91cb 100644 --- a/src/api/v1/guilds/uuid/channels.rs +++ b/src/api/v1/guilds/uuid/channels.rs @@ -77,7 +77,7 @@ pub async fn create( let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; member - .check_permission(&data, Permissions::CreateChannel) + .check_permission(&data, Permissions::ManageChannel) .await?; let channel = Channel::new( diff --git a/src/api/v1/guilds/uuid/icon.rs b/src/api/v1/guilds/uuid/icon.rs index 43a5e05..600ccba 100644 --- a/src/api/v1/guilds/uuid/icon.rs +++ b/src/api/v1/guilds/uuid/icon.rs @@ -39,7 +39,7 @@ pub async fn upload( let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; member - .check_permission(&data, Permissions::ManageServer) + .check_permission(&data, Permissions::ManageGuild) .await?; let mut guild = Guild::fetch_one(&mut conn, guild_uuid).await?; diff --git a/src/api/v1/guilds/uuid/roles/mod.rs b/src/api/v1/guilds/uuid/roles/mod.rs index 5f38923..0fcc5b3 100644 --- a/src/api/v1/guilds/uuid/roles/mod.rs +++ b/src/api/v1/guilds/uuid/roles/mod.rs @@ -73,7 +73,7 @@ pub async fn create( let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; member - .check_permission(&data, Permissions::CreateRole) + .check_permission(&data, Permissions::ManageRole) .await?; let role = Role::new(&mut conn, guild_uuid, role_info.name.clone()).await?; diff --git a/src/objects/role.rs b/src/objects/role.rs index 4a75628..68e9c27 100644 --- a/src/objects/role.rs +++ b/src/objects/role.rs @@ -155,32 +155,31 @@ impl Role { #[derive(Clone, Copy, PartialEq, Eq)] pub enum Permissions { + /// Lets users send messages in the guild or channel SendMessage = 1, - CreateChannel = 2, - DeleteChannel = 4, - ManageChannel = 8, - CreateRole = 16, - DeleteRole = 32, - ManageRole = 64, - CreateInvite = 128, - ManageInvite = 256, - ManageServer = 512, - ManageMember = 1024, + /// Lets users create, delete and edit channels and categories or a singular channel depending on permission context + ManageChannel = 2, + /// Lets users manage roles in the guild + ManageRole = 4, + /// Lets users create invites in the guild + CreateInvite = 8, + /// Lets users manage invites in the guild + ManageInvite = 16, + /// Lets users change guild settings + ManageGuild = 32, + /// Lets users change member settings (nickname, etc) + ManageMember = 64, } impl Permissions { pub fn fetch_permissions(permissions: i64) -> Vec { let all_perms = vec![ Self::SendMessage, - Self::CreateChannel, - Self::DeleteChannel, Self::ManageChannel, - Self::CreateRole, - Self::DeleteRole, Self::ManageRole, Self::CreateInvite, Self::ManageInvite, - Self::ManageServer, + Self::ManageGuild, Self::ManageMember, ]; From 5f8d0271e7f123aeef883e681f04413bcaed49fb Mon Sep 17 00:00:00 2001 From: Radical Date: Sat, 12 Jul 2025 20:16:09 +0200 Subject: [PATCH 083/160] fix: use correct permission for channel deletion --- src/api/v1/channels/uuid/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/v1/channels/uuid/mod.rs b/src/api/v1/channels/uuid/mod.rs index d7cfa39..fff2ef0 100644 --- a/src/api/v1/channels/uuid/mod.rs +++ b/src/api/v1/channels/uuid/mod.rs @@ -62,7 +62,7 @@ pub async fn delete( let member = Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; member - .check_permission(&data, Permissions::DeleteChannel) + .check_permission(&data, Permissions::ManageChannel) .await?; channel.delete(&data).await?; From e17fc9fff0fff0dc9eded1492fa061ae33073158 Mon Sep 17 00:00:00 2001 From: JustTemmie <47639983+JustTemmie@users.noreply.github.com> Date: Sun, 13 Jul 2025 16:11:47 +0200 Subject: [PATCH 084/160] fix: add a friend via uesrname instead of their UUID --- src/api/v1/me/friends/mod.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/api/v1/me/friends/mod.rs b/src/api/v1/me/friends/mod.rs index f946616..217e32b 100644 --- a/src/api/v1/me/friends/mod.rs +++ b/src/api/v1/me/friends/mod.rs @@ -1,15 +1,10 @@ -use ::uuid::Uuid; use actix_web::{HttpRequest, HttpResponse, get, post, web}; use serde::Deserialize; pub mod uuid; use crate::{ - Data, - api::v1::auth::check_access_token, - error::Error, - objects::Me, - utils::{get_auth_header, global_checks}, + api::v1::auth::check_access_token, error::Error, objects::Me, utils::{get_auth_header, global_checks, user_uuid_from_identifier}, Data }; /// Returns a list of users that are your friends @@ -34,7 +29,7 @@ pub async fn get(req: HttpRequest, data: web::Data) -> Result Date: Sun, 13 Jul 2025 16:17:54 +0200 Subject: [PATCH 085/160] fix: linter :( why you one line the import D: --- src/api/v1/me/friends/mod.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/api/v1/me/friends/mod.rs b/src/api/v1/me/friends/mod.rs index 217e32b..5980adc 100644 --- a/src/api/v1/me/friends/mod.rs +++ b/src/api/v1/me/friends/mod.rs @@ -4,7 +4,11 @@ use serde::Deserialize; pub mod uuid; use crate::{ - api::v1::auth::check_access_token, error::Error, objects::Me, utils::{get_auth_header, global_checks, user_uuid_from_identifier}, Data + Data, + api::v1::auth::check_access_token, + error::Error, + objects::Me, + utils::{get_auth_header, global_checks, user_uuid_from_identifier}, }; /// Returns a list of users that are your friends From d775723b7b11afc7e6cb8497fac404497c0f926e Mon Sep 17 00:00:00 2001 From: JustTemmie <47639983+JustTemmie@users.noreply.github.com> Date: Sun, 13 Jul 2025 16:20:03 +0200 Subject: [PATCH 086/160] fix: require username, instead of username OR email --- src/api/v1/me/friends/mod.rs | 4 ++-- src/utils.rs | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/api/v1/me/friends/mod.rs b/src/api/v1/me/friends/mod.rs index 5980adc..8de0a5d 100644 --- a/src/api/v1/me/friends/mod.rs +++ b/src/api/v1/me/friends/mod.rs @@ -8,7 +8,7 @@ use crate::{ api::v1::auth::check_access_token, error::Error, objects::Me, - utils::{get_auth_header, global_checks, user_uuid_from_identifier}, + utils::{get_auth_header, global_checks, user_uuid_from_username} }; /// Returns a list of users that are your friends @@ -73,7 +73,7 @@ pub async fn post( let me = Me::get(&mut conn, uuid).await?; - let target_uuid = user_uuid_from_identifier(&mut conn, &json.username).await?; + let target_uuid = user_uuid_from_username(&mut conn, &json.username).await?; me.add_friend(&mut conn, target_uuid).await?; Ok(HttpResponse::Ok().finish()) diff --git a/src/utils.rs b/src/utils.rs index 7a5581a..01a7f0d 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -168,6 +168,26 @@ pub async fn user_uuid_from_identifier( } } +pub async fn user_uuid_from_username( + conn: &mut Conn, + identifier: &String, +) -> Result { + if USERNAME_REGEX.is_match(identifier) { + use users::dsl; + let user_uuid = dsl::users + .filter(dsl::username.eq(identifier)) + .select(dsl::uuid) + .get_result(conn) + .await?; + + Ok(user_uuid) + } else { + Err(Error::BadRequest( + "Please provide a valid username".to_string(), + )) + } +} + pub async fn global_checks(data: &Data, user_uuid: Uuid) -> Result<(), Error> { if data.config.instance.require_email_verification { let mut conn = data.pool.get().await?; From 384f5e404fac5a44cf51b573e2a4a7772a590bdd Mon Sep 17 00:00:00 2001 From: JustTemmie <47639983+JustTemmie@users.noreply.github.com> Date: Sun, 13 Jul 2025 16:21:07 +0200 Subject: [PATCH 087/160] fix: change function paramater name to match function name --- src/utils.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 01a7f0d..072143f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -170,12 +170,12 @@ pub async fn user_uuid_from_identifier( pub async fn user_uuid_from_username( conn: &mut Conn, - identifier: &String, + username: &String, ) -> Result { - if USERNAME_REGEX.is_match(identifier) { + if USERNAME_REGEX.is_match(username) { use users::dsl; let user_uuid = dsl::users - .filter(dsl::username.eq(identifier)) + .filter(dsl::username.eq(username)) .select(dsl::uuid) .get_result(conn) .await?; From 671fc425558287e20bb88f07e9449665df2fc9a6 Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 13 Jul 2025 18:03:28 +0200 Subject: [PATCH 088/160] fix: make (user_uuid, guild_uuid) unique in members --- migrations/2025-07-13-155008_unique_guild_members/down.sql | 2 ++ migrations/2025-07-13-155008_unique_guild_members/up.sql | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 migrations/2025-07-13-155008_unique_guild_members/down.sql create mode 100644 migrations/2025-07-13-155008_unique_guild_members/up.sql diff --git a/migrations/2025-07-13-155008_unique_guild_members/down.sql b/migrations/2025-07-13-155008_unique_guild_members/down.sql new file mode 100644 index 0000000..013105c --- /dev/null +++ b/migrations/2025-07-13-155008_unique_guild_members/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE guild_members DROP CONSTRAINT guild_members_user_uuid_guild_uuid_key; diff --git a/migrations/2025-07-13-155008_unique_guild_members/up.sql b/migrations/2025-07-13-155008_unique_guild_members/up.sql new file mode 100644 index 0000000..d139337 --- /dev/null +++ b/migrations/2025-07-13-155008_unique_guild_members/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE guild_members ADD UNIQUE (user_uuid, guild_uuid) \ No newline at end of file From 3647086adb815816e75eb25fd172bebee393f111 Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 13 Jul 2025 18:06:23 +0200 Subject: [PATCH 089/160] feat: add endpoint to get logged in devices --- src/api/v1/auth/devices.rs | 58 ++++++++++++++++++++++++++++++++++++++ src/api/v1/auth/mod.rs | 2 ++ 2 files changed, 60 insertions(+) create mode 100644 src/api/v1/auth/devices.rs diff --git a/src/api/v1/auth/devices.rs b/src/api/v1/auth/devices.rs new file mode 100644 index 0000000..532ad00 --- /dev/null +++ b/src/api/v1/auth/devices.rs @@ -0,0 +1,58 @@ +//! `/api/v1/auth/devices` Returns list of logged in devices + +use actix_web::{HttpRequest, HttpResponse, get, web}; +use diesel::{ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper}; +use diesel_async::RunQueryDsl; +use serde::Serialize; + +use crate::{ + Data, + api::v1::auth::check_access_token, + error::Error, + schema::refresh_tokens::{self, dsl}, + utils::get_auth_header, +}; + +#[derive(Serialize, Selectable, Queryable)] +#[diesel(table_name = refresh_tokens)] +#[diesel(check_for_backend(diesel::pg::Pg))] +struct Device { + device_name: String, + created_at: i64 +} + +/// `GET /api/v1/auth/devices` Returns list of logged in devices +/// +/// requires auth: no +/// +/// ### Response Example +/// ``` +/// json!([ +/// { +/// "device_name": "My Device!" +/// "created_at": "1752418856" +/// } +/// +/// ]); +/// ``` +#[get("/devices")] +pub async fn get( + req: HttpRequest, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + let devices: Vec = dsl::refresh_tokens + .filter(dsl::uuid.eq(uuid)) + .select(Device::as_select()) + .get_results(&mut conn) + .await?; + + Ok(HttpResponse::Ok().json(devices)) +} diff --git a/src/api/v1/auth/mod.rs b/src/api/v1/auth/mod.rs index 75a6b0b..0e6b006 100644 --- a/src/api/v1/auth/mod.rs +++ b/src/api/v1/auth/mod.rs @@ -8,6 +8,7 @@ use uuid::Uuid; use crate::{Conn, error::Error, schema::access_tokens::dsl}; +mod devices; mod login; mod logout; mod refresh; @@ -32,6 +33,7 @@ pub fn web() -> Scope { .service(verify_email::post) .service(reset_password::get) .service(reset_password::post) + .service(devices::get) } pub async fn check_access_token(access_token: &str, conn: &mut Conn) -> Result { From 8656115dc9b3f0a6527782f1484d828438521bc7 Mon Sep 17 00:00:00 2001 From: JustTemmie <47639983+JustTemmie@users.noreply.github.com> Date: Mon, 14 Jul 2025 00:36:15 +0200 Subject: [PATCH 090/160] feat: start implementing device name generation in the backend --- Cargo.toml | 1 + src/api/v1/auth/login.rs | 12 +- src/api/v1/auth/mod.rs | 1 + src/api/v1/auth/refresh.rs | 11 +- src/api/v1/auth/register.rs | 25 +- src/main.rs | 1 + src/word_list.rs | 1005 +++++++++++++++++++++++++++++++++++ 7 files changed, 1034 insertions(+), 22 deletions(-) create mode 100644 src/word_list.rs diff --git a/Cargo.toml b/Cargo.toml index c1c71bc..43e5ea5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,5 +60,6 @@ regex = "1.11" random-string = "1.1" lettre = { version = "0.11", features = ["tokio1", "tokio1-native-tls"] } chrono = { version = "0.4.41", features = ["serde"] } +rand = "0.9.1" diff --git a/src/api/v1/auth/login.rs b/src/api/v1/auth/login.rs index 2faaeb4..cef4726 100644 --- a/src/api/v1/auth/login.rs +++ b/src/api/v1/auth/login.rs @@ -7,10 +7,7 @@ use diesel_async::RunQueryDsl; use serde::Deserialize; use crate::{ - Data, - error::Error, - schema::*, - utils::{PASSWORD_REGEX, generate_token, new_refresh_token_cookie, user_uuid_from_identifier}, + error::Error, schema::*, utils::{generate_token, new_refresh_token_cookie, user_uuid_from_identifier, PASSWORD_REGEX}, generate_device_name::generate_device_name, Data }; use super::Response; @@ -19,7 +16,6 @@ use super::Response; struct LoginInformation { username: String, password: String, - device_name: String, } #[post("/login")] @@ -63,12 +59,14 @@ pub async fn response( use refresh_tokens::dsl as rdsl; + let device_name = generate_device_name(); + insert_into(refresh_tokens::table) .values(( rdsl::token.eq(&refresh_token), rdsl::uuid.eq(uuid), rdsl::created_at.eq(current_time), - rdsl::device_name.eq(&login_information.device_name), + rdsl::device_name.eq(&device_name), )) .execute(&mut conn) .await?; @@ -87,5 +85,5 @@ pub async fn response( Ok(HttpResponse::Ok() .cookie(new_refresh_token_cookie(&data.config, refresh_token)) - .json(Response { access_token })) + .json(Response { access_token, device_name })) } diff --git a/src/api/v1/auth/mod.rs b/src/api/v1/auth/mod.rs index 0e6b006..947e7aa 100644 --- a/src/api/v1/auth/mod.rs +++ b/src/api/v1/auth/mod.rs @@ -20,6 +20,7 @@ mod verify_email; #[derive(Serialize)] struct Response { access_token: String, + device_name: String, } pub fn web() -> Scope { diff --git a/src/api/v1/auth/refresh.rs b/src/api/v1/auth/refresh.rs index abd9a34..e9a444b 100644 --- a/src/api/v1/auth/refresh.rs +++ b/src/api/v1/auth/refresh.rs @@ -77,6 +77,15 @@ pub async fn res(req: HttpRequest, data: web::Data) -> Result()?; + let device_name: String; + + // fix me tomorrow + // let devices: Vec = dsl::refresh_tokens + // .filter(dsl::uuid.eq(uuid)) + // .select(Device::as_select()) + // .get_results(&mut conn) + // .await?; + update(access_tokens::table) .filter(dsl::refresh_token.eq(&refresh_token)) .set(( @@ -88,7 +97,7 @@ pub async fn res(req: HttpRequest, data: web::Data) -> Result String { + let adjective_index = rand::rng().random_range(0..ADJECTIVES_LENGTH-1); + let animal_index = rand::rng().random_range(0..ANIMALS_LENGTH-1); + + return [ADJECTIVES[adjective_index], ANIMALS[animal_index]].join(" ") +} + +const ANIMALS_LENGTH: usize = 223; +const ADJECTIVES_LENGTH: usize = 765; + +const ANIMALS: [&'static str; ANIMALS_LENGTH] = [ + "Aardvark", + "Albatross", + "Alligator", + "Alpaca", + "Ant", + "Anteater", + "Antelope", + "Ape", + "Armadillo", + "Donkey", + "Baboon", + "Badger", + "Barracuda", + "Bat", + "Bear", + "Beaver", + "Bee", + "Bison", + "Boar", + "Buffalo", + "Butterfly", + "Camel", + "Capybara", + "Caribou", + "Cassowary", + "Cat", + "Caterpillar", + "Cattle", + "Chamois", + "Cheetah", + "Chicken", + "Chimpanzee", + "Chinchilla", + "Chough", + "Clam", + "Cobra", + "Cockroach", + "Cod", + "Cormorant", + "Coyote", + "Crab", + "Crane", + "Crocodile", + "Crow", + "Curlew", + "Deer", + "Dinosaur", + "Dog", + "Dogfish", + "Dolphin", + "Dotterel", + "Dove", + "Dragonfly", + "Duck", + "Dugong", + "Dunlin", + "Eagle", + "Echidna", + "Eel", + "Eland", + "Elephant", + "Elk", + "Emu", + "Falcon", + "Ferret", + "Finch", + "Fish", + "Flamingo", + "Fly", + "Fox", + "Frog", + "Gaur", + "Gazelle", + "Gerbil", + "Giraffe", + "Gnat", + "Gnu", + "Goat", + "Goldfinch", + "Goldfish", + "Goose", + "Gorilla", + "Goshawk", + "Grasshopper", + "Grouse", + "Guanaco", + "Gull", + "Hamster", + "Hare", + "Hawk", + "Hedgehog", + "Heron", + "Herring", + "Hippopotamus", + "Hornet", + "Horse", + "Hummingbird", + "Hyena", + "Ibex", + "Ibis", + "Jackal", + "Jaguar", + "Jay", + "Jellyfish", + "Kangaroo", + "Kingfisher", + "Koala", + "Kookabura", + "Kouprey", + "Kudu", + "Lapwing", + "Lark", + "Lemur", + "Leopard", + "Lion", + "Llama", + "Lobster", + "Locust", + "Loris", + "Louse", + "Lyrebird", + "Magpie", + "Mallard", + "Manatee", + "Mandrill", + "Mantis", + "Marten", + "Meerkat", + "Mink", + "Mole", + "Mongoose", + "Monkey", + "Moose", + "Mosquito", + "Mouse", + "Mule", + "Narwhal", + "Newt", + "Nightingale", + "Octopus", + "Okapi", + "Opossum", + "Oryx", + "Ostrich", + "Otter", + "Owl", + "Oyster", + "Panther", + "Parrot", + "Partridge", + "Peafowl", + "Pelican", + "Penguin", + "Pheasant", + "Pig", + "Pigeon", + "Pony", + "Porcupine", + "Porpoise", + "Quail", + "Quelea", + "Quetzal", + "Rabbit", + "Raccoon", + "Rail", + "Ram", + "Rat", + "Raven", + "Red deer", + "Red panda", + "Reindeer", + "Rhinoceros", + "Rook", + "Salamander", + "Salmon", + "Sand Dollar", + "Sandpiper", + "Sardine", + "Scorpion", + "Seahorse", + "Seal", + "Shark", + "Sheep", + "Shrew", + "Skunk", + "Snail", + "Snake", + "Sparrow", + "Spider", + "Spoonbill", + "Squid", + "Squirrel", + "Starling", + "Stingray", + "Stinkbug", + "Stork", + "Swallow", + "Swan", + "Tapir", + "Tarsier", + "Termite", + "Tiger", + "Toad", + "Trout", + "Turkey", + "Turtle", + "Viper", + "Vulture", + "Wallaby", + "Walrus", + "Wasp", + "Weasel", + "Whale", + "Wildcat", + "Wolf", + "Wolverine", + "Wombat", + "Woodcock", + "Woodpecker", + "Worm", + "Wren", + "Yak", + "Zebra", +]; + +const ADJECTIVES: [&'static str; ADJECTIVES_LENGTH] = [ + "other", + "such", + "first", + "many", + "new", + "more", + "same", + "own", + "good", + "different", + "great", + "long", + "high", + "social", + "little", + "much", + "important", + "small", + "most", + "large", + "old", + "few", + "general", + "second", + "public", + "last", + "several", + "early", + "certain", + "economic", + "least", + "common", + "present", + "next", + "local", + "best", + "particular", + "young", + "various", + "necessary", + "whole", + "only", + "true", + "able", + "major", + "full", + "low", + "available", + "real", + "similar", + "total", + "special", + "less", + "short", + "specific", + "single", + "self", + "national", + "individual", + "clear", + "personal", + "higher", + "better", + "third", + "natural", + "greater", + "open", + "difficult", + "current", + "further", + "main", + "physical", + "foreign", + "lower", + "strong", + "private", + "likely", + "international", + "significant", + "late", + "basic", + "hard", + "modern", + "simple", + "normal", + "sure", + "central", + "original", + "effective", + "following", + "direct", + "final", + "cultural", + "big", + "recent", + "complete", + "financial", + "positive", + "primary", + "appropriate", + "legal", + "european", + "equal", + "larger", + "average", + "historical", + "critical", + "wide", + "traditional", + "additional", + "active", + "complex", + "former", + "independent", + "entire", + "actual", + "close", + "constant", + "previous", + "easy", + "serious", + "potential", + "fine", + "industrial", + "subject", + "future", + "internal", + "initial", + "well", + "essential", + "dark", + "popular", + "successful", + "standard", + "year", + "past", + "ready", + "professional", + "wrong", + "very", + "proper", + "separate", + "heavy", + "civil", + "responsible", + "considerable", + "light", + "cold", + "above", + "older", + "practical", + "external", + "sufficient", + "interesting", + "upper", + "scientific", + "key", + "annual", + "limited", + "smaller", + "southern", + "earlier", + "commercial", + "powerful", + "later", + "like", + "clinical", + "ancient", + "educational", + "typical", + "technical", + "environmental", + "formal", + "aware", + "beautiful", + "variable", + "obvious", + "secondary", + "enough", + "urban", + "regular", + "relevant", + "greatest", + "spiritual", + "time", + "double", + "happy", + "term", + "multiple", + "dependent", + "correct", + "northern", + "middle", + "rural", + "official", + "fundamental", + "numerous", + "overall", + "usual", + "native", + "regional", + "highest", + "north", + "agricultural", + "literary", + "broad", + "perfect", + "experimental", + "fourth", + "global", + "ordinary", + "related", + "apparent", + "daily", + "principal", + "contemporary", + "severe", + "reasonable", + "subsequent", + "worth", + "longer", + "emotional", + "intellectual", + "unique", + "pure", + "familiar", + "american", + "solid", + "brief", + "famous", + "fresh", + "day", + "corresponding", + "characteristic", + "maximum", + "detailed", + "outside", + "theoretical", + "fair", + "opposite", + "capable", + "visual", + "interested", + "joint", + "adequate", + "based", + "substantial", + "unable", + "structural", + "soft", + "false", + "largest", + "inner", + "mean", + "extensive", + "excellent", + "rapid", + "absolute", + "consistent", + "continuous", + "administrative", + "strange", + "willing", + "alternative", + "slow", + "distinct", + "safe", + "permanent", + "front", + "corporate", + "academic", + "thin", + "nineteenth", + "universal", + "functional", + "unknown", + "careful", + "narrow", + "evident", + "sound", + "classical", + "minor", + "weak", + "suitable", + "chief", + "extreme", + "yellow", + "warm", + "mixed", + "flat", + "huge", + "vast", + "stable", + "valuable", + "rare", + "visible", + "sensitive", + "mechanical", + "state", + "radical", + "extra", + "superior", + "conventional", + "thick", + "dominant", + "post", + "collective", + "younger", + "efficient", + "linear", + "organic", + "oral", + "century", + "creative", + "vertical", + "dynamic", + "empty", + "minimum", + "cognitive", + "logical", + "afraid", + "equivalent", + "quick", + "near", + "concrete", + "mass", + "acute", + "sharp", + "easier", + "quiet", + "adult", + "accurate", + "ideal", + "partial", + "bright", + "identical", + "conservative", + "magnetic", + "frequent", + "electronic", + "fixed", + "square", + "cross", + "clean", + "back", + "organizational", + "constitutional", + "genetic", + "ultimate", + "secret", + "vital", + "dramatic", + "objective", + "round", + "alive", + "straight", + "unusual", + "rational", + "electric", + "mutual", + "class", + "competitive", + "revolutionary", + "statistical", + "random", + "musical", + "crucial", + "racial", + "sudden", + "acid", + "content", + "temporary", + "line", + "remarkable", + "exact", + "valid", + "helpful", + "nice", + "comprehensive", + "united", + "level", + "fifth", + "nervous", + "expensive", + "prominent", + "healthy", + "liquid", + "institutional", + "silent", + "sweet", + "strategic", + "molecular", + "comparative", + "called", + "electrical", + "raw", + "acceptable", + "scale", + "violent", + "all", + "desirable", + "tall", + "steady", + "wonderful", + "sub", + "distant", + "progressive", + "enormous", + "horizontal", + "and", + "intense", + "smooth", + "applicable", + "over", + "animal", + "abstract", + "wise", + "worst", + "gold", + "precise", + "legislative", + "remote", + "technological", + "outer", + "uniform", + "slight", + "attractive", + "evil", + "tiny", + "royal", + "angry", + "advanced", + "friendly", + "dear", + "busy", + "spatial", + "rough", + "primitive", + "judicial", + "systematic", + "lateral", + "sorry", + "plain", + "off", + "comfortable", + "definite", + "massive", + "firm", + "widespread", + "prior", + "twentieth", + "mathematical", + "verbal", + "marginal", + "excessive", + "stronger", + "gross", + "world", + "productive", + "wider", + "glad", + "linguistic", + "patient", + "symbolic", + "earliest", + "plastic", + "type", + "prime", + "eighteenth", + "blind", + "neutral", + "guilty", + "hand", + "extraordinary", + "metal", + "surprising", + "fellow", + "york", + "grand", + "thermal", + "artificial", + "five", + "lowest", + "genuine", + "dimensional", + "optical", + "unlikely", + "developmental", + "reliable", + "executive", + "comparable", + "satisfactory", + "golden", + "diverse", + "preliminary", + "wooden", + "noble", + "part", + "striking", + "cool", + "classic", + "elderly", + "four", + "temporal", + "indirect", + "romantic", + "intermediate", + "differential", + "passive", + "life", + "voluntary", + "out", + "adjacent", + "behavioral", + "exclusive", + "closed", + "inherent", + "inevitable", + "complicated", + "quantitative", + "respective", + "artistic", + "probable", + "anxious", + "informal", + "strict", + "fiscal", + "ideological", + "profound", + "extended", + "eternal", + "known", + "infinite", + "proud", + "honest", + "peculiar", + "absent", + "pleasant", + "optimal", + "renal", + "static", + "outstanding", + "presidential", + "digital", + "integrated", + "legitimate", + "curious", + "aggressive", + "deeper", + "elementary", + "history", + "surgical", + "occasional", + "flexible", + "convenient", + "solar", + "atomic", + "isolated", + "latest", + "sad", + "conceptual", + "underlying", + "everyday", + "cost", + "intensive", + "odd", + "subjective", + "mid", + "worthy", + "pale", + "meaningful", + "therapeutic", + "making", + "circular", + "realistic", + "multi", + "child", + "sophisticated", + "down", + "leading", + "intelligent", + "governmental", + "numerical", + "minimal", + "diagnostic", + "indigenous", + "aesthetic", + "distinctive", + "operational", + "sole", + "material", + "fast", + "bitter", + "broader", + "brilliant", + "peripheral", + "rigid", + "automatic", + "lesser", + "routine", + "favorable", + "cooperative", + "cardiac", + "arbitrary", + "loose", + "favorite", + "subtle", + "uncertain", + "hostile", + "monthly", + "naval", + "physiological", + "historic", + "developed", + "skilled", + "anterior", + "pro", + "gentle", + "loud", + "pulmonary", + "innocent", + "provincial", + "mild", + "page", + "specialized", + "bare", + "excess", + "inter", + "shaped", + "theological", + "sensory", + "the", + "stress", + "novel", + "working", + "shorter", + "secular", + "geographical", + "intimate", + "liable", + "selective", + "influential", + "modest", + "successive", + "continued", + "water", + "expert", + "municipal", + "marine", + "thirty", + "adverse", + "wacky", + "closer", + "virtual", + "peaceful", + "mobile", + "sixth", + "immune", + "coastal", + "representative", + "lead", + "forward", + "faithful", + "crystal", + "protective", + "elaborate", + "tremendous", + "welcoming", + "abnormal", + "grateful", + "proportional", + "dual", + "operative", + "precious", + "sympathetic", + "accessible", + "lovely", + "spinal", + "even", + "marked", + "observed", + "point", + "mature", + "competent", + "residential", + "impressive", + "unexpected", + "nearby", + "unnecessary", + "generous", + "cerebral", + "unpublished", + "delicate", + "analytical", + "tropical", + "statutory", + "cell", + "weekly", + "end", + "online", + "beneficial", + "aged", + "tough", + "eager", + "ongoing", + "silver", + "persistent", + "calm", + "nearest", + "hidden", + "magic", + "pretty", + "wealthy", + "exciting", + "decisive", + "confident", + "invisible", + "notable", + "medium", + "manual", + "select", + "thorough", + "causal", + "giant", + "bigger", + "pink", + "improved", + "immense", + "hour", + "intact", + "grade", + "dense", + "hungry", + "biggest", + "abundant", + "handsome", + "retail", + "insufficient", + "irregular", + "intrinsic", + "residual", + "follow", + "fluid", + "mysterious", + "descriptive", + "elastic", + "destructive", + "architectural", + "synthetic", + "continental", + "evolutionary", + "lucky", + "bold", + "funny", + "peak", + "smallest", + "reluctant", + "suspicious", + "smart", + "mighty", + "brave", + "humble", + "vocal", + "obscure", + "innovative", +]; \ No newline at end of file From e7bc53f8588527680505e9dcddaaf3167409aefc Mon Sep 17 00:00:00 2001 From: JustTemmie <47639983+JustTemmie@users.noreply.github.com> Date: Mon, 14 Jul 2025 01:02:03 +0200 Subject: [PATCH 091/160] feat: try reading the device name from the table --- src/api/v1/auth/refresh.rs | 27 +++++++------------ src/{word_list.rs => generate_device_name.rs} | 0 2 files changed, 9 insertions(+), 18 deletions(-) rename src/{word_list.rs => generate_device_name.rs} (100%) diff --git a/src/api/v1/auth/refresh.rs b/src/api/v1/auth/refresh.rs index e9a444b..a89efbc 100644 --- a/src/api/v1/auth/refresh.rs +++ b/src/api/v1/auth/refresh.rs @@ -5,13 +5,10 @@ use log::error; use std::time::{SystemTime, UNIX_EPOCH}; use crate::{ - Data, - error::Error, - schema::{ + error::Error, schema::{ access_tokens::{self, dsl}, - refresh_tokens::{self, dsl as rdsl}, - }, - utils::{generate_token, new_refresh_token_cookie}, + refresh_tokens::{self, device_name, dsl as rdsl}, + }, utils::{generate_token, new_refresh_token_cookie}, Data }; use super::Response; @@ -53,6 +50,7 @@ pub async fn res(req: HttpRequest, data: web::Data) -> Result 1987200 { let new_refresh_token = generate_token::<32>()?; @@ -63,11 +61,13 @@ pub async fn res(req: HttpRequest, data: web::Data) -> Result(&mut conn) .await { - Ok(_) => { + Ok(device_name) => { refresh_token = new_refresh_token; + existing_device_name = device_name.to_string(); } Err(error) => { error!("{error}"); @@ -77,15 +77,6 @@ pub async fn res(req: HttpRequest, data: web::Data) -> Result()?; - let device_name: String; - - // fix me tomorrow - // let devices: Vec = dsl::refresh_tokens - // .filter(dsl::uuid.eq(uuid)) - // .select(Device::as_select()) - // .get_results(&mut conn) - // .await?; - update(access_tokens::table) .filter(dsl::refresh_token.eq(&refresh_token)) .set(( @@ -97,7 +88,7 @@ pub async fn res(req: HttpRequest, data: web::Data) -> Result Date: Tue, 15 Jul 2025 02:30:07 +0200 Subject: [PATCH 092/160] fix: increase length of refresh token field --- .../2025-07-15-002434_increase_device_name_length/down.sql | 2 ++ migrations/2025-07-15-002434_increase_device_name_length/up.sql | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 migrations/2025-07-15-002434_increase_device_name_length/down.sql create mode 100644 migrations/2025-07-15-002434_increase_device_name_length/up.sql diff --git a/migrations/2025-07-15-002434_increase_device_name_length/down.sql b/migrations/2025-07-15-002434_increase_device_name_length/down.sql new file mode 100644 index 0000000..4fe6628 --- /dev/null +++ b/migrations/2025-07-15-002434_increase_device_name_length/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE refresh_tokens ALTER COLUMN device_name TYPE varchar(16); \ No newline at end of file diff --git a/migrations/2025-07-15-002434_increase_device_name_length/up.sql b/migrations/2025-07-15-002434_increase_device_name_length/up.sql new file mode 100644 index 0000000..9d44298 --- /dev/null +++ b/migrations/2025-07-15-002434_increase_device_name_length/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE refresh_tokens ALTER COLUMN device_name TYPE varchar(64); \ No newline at end of file From fc061738fa74d1d5ead22ec034d7ac01e8119f3b Mon Sep 17 00:00:00 2001 From: JustTemmie <47639983+JustTemmie@users.noreply.github.com> Date: Tue, 15 Jul 2025 02:42:53 +0200 Subject: [PATCH 093/160] feat: finish adding device name to login, register, and refresh endpoints --- src/api/v1/auth/login.rs | 2 +- src/api/v1/auth/refresh.rs | 10 +++++----- src/api/v1/auth/register.rs | 4 ++-- src/main.rs | 2 +- src/schema.rs | 2 +- src/utils.rs | 14 +++++++++----- src/{generate_device_name.rs => wordlist.rs} | 16 ++-------------- 7 files changed, 21 insertions(+), 29 deletions(-) rename src/{generate_device_name.rs => wordlist.rs} (96%) diff --git a/src/api/v1/auth/login.rs b/src/api/v1/auth/login.rs index cef4726..b2f3180 100644 --- a/src/api/v1/auth/login.rs +++ b/src/api/v1/auth/login.rs @@ -7,7 +7,7 @@ use diesel_async::RunQueryDsl; use serde::Deserialize; use crate::{ - error::Error, schema::*, utils::{generate_token, new_refresh_token_cookie, user_uuid_from_identifier, PASSWORD_REGEX}, generate_device_name::generate_device_name, Data + error::Error, schema::*, utils::{generate_device_name, generate_token, new_refresh_token_cookie, user_uuid_from_identifier, PASSWORD_REGEX}, Data }; use super::Response; diff --git a/src/api/v1/auth/refresh.rs b/src/api/v1/auth/refresh.rs index a89efbc..90728de 100644 --- a/src/api/v1/auth/refresh.rs +++ b/src/api/v1/auth/refresh.rs @@ -7,7 +7,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use crate::{ error::Error, schema::{ access_tokens::{self, dsl}, - refresh_tokens::{self, device_name, dsl as rdsl}, + refresh_tokens::{self, dsl as rdsl}, }, utils::{generate_token, new_refresh_token_cookie}, Data }; @@ -50,7 +50,7 @@ pub async fn res(req: HttpRequest, data: web::Data) -> Result 1987200 { let new_refresh_token = generate_token::<32>()?; @@ -65,9 +65,9 @@ pub async fn res(req: HttpRequest, data: web::Data) -> Result(&mut conn) .await { - Ok(device_name) => { + Ok(existing_device_name) => { refresh_token = new_refresh_token; - existing_device_name = device_name.to_string(); + device_name = existing_device_name; } Err(error) => { error!("{error}"); @@ -88,7 +88,7 @@ pub async fn res(req: HttpRequest, data: web::Data) -> Result Varchar, uuid -> Uuid, created_at -> Int8, - #[max_length = 16] + #[max_length = 64] device_name -> Varchar, } } diff --git a/src/utils.rs b/src/utils.rs index 072143f..d058db3 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,5 @@ use std::sync::LazyLock; +use rand::Rng; use actix_web::{ cookie::{Cookie, SameSite, time::Duration}, @@ -16,11 +17,7 @@ use serde::Serialize; use uuid::Uuid; use crate::{ - Conn, Data, - config::Config, - error::Error, - objects::{HasIsAbove, HasUuid}, - schema::users, + config::Config, error::Error, objects::{HasIsAbove, HasUuid}, schema::users, wordlist::{ADJECTIVES, ANIMALS}, Conn, Data }; pub static EMAIL_REGEX: LazyLock = LazyLock::new(|| { @@ -282,3 +279,10 @@ impl Data { .await } } + +pub fn generate_device_name() -> String { + let adjective_index = rand::rng().random_range(0..ADJECTIVES.len()-1); + let animal_index = rand::rng().random_range(0..ANIMALS.len()-1); + + return [ADJECTIVES[adjective_index], ANIMALS[animal_index]].join(" ") +} \ No newline at end of file diff --git a/src/generate_device_name.rs b/src/wordlist.rs similarity index 96% rename from src/generate_device_name.rs rename to src/wordlist.rs index 1992f95..3ca3c3c 100644 --- a/src/generate_device_name.rs +++ b/src/wordlist.rs @@ -1,16 +1,4 @@ -use rand::Rng; - -pub fn generate_device_name() -> String { - let adjective_index = rand::rng().random_range(0..ADJECTIVES_LENGTH-1); - let animal_index = rand::rng().random_range(0..ANIMALS_LENGTH-1); - - return [ADJECTIVES[adjective_index], ANIMALS[animal_index]].join(" ") -} - -const ANIMALS_LENGTH: usize = 223; -const ADJECTIVES_LENGTH: usize = 765; - -const ANIMALS: [&'static str; ANIMALS_LENGTH] = [ +pub const ANIMALS: [&'static str; 223] = [ "Aardvark", "Albatross", "Alligator", @@ -236,7 +224,7 @@ const ANIMALS: [&'static str; ANIMALS_LENGTH] = [ "Zebra", ]; -const ADJECTIVES: [&'static str; ADJECTIVES_LENGTH] = [ +pub const ADJECTIVES: [&'static str; 765] = [ "other", "such", "first", From 324137ce8bc9eeadb08c3b149a7f122d7aa2e68f Mon Sep 17 00:00:00 2001 From: Radical Date: Wed, 16 Jul 2025 16:36:22 +0200 Subject: [PATCH 094/160] refactor: rewrite entire codebase in axum instead of actix Replaces actix with axum for web, allows us to use socket.io and gives us access to the tower ecosystem of middleware breaks compatibility with our current websocket implementation, needs to be reimplemented for socket.io --- Cargo.toml | 17 ++-- src/api/mod.rs | 15 +-- src/api/v1/auth/devices.rs | 30 +++--- src/api/v1/auth/login.rs | 56 +++++++---- src/api/v1/auth/logout.rs | 56 ++++++++--- src/api/v1/auth/mod.rs | 42 ++++---- src/api/v1/auth/refresh.rs | 107 +++++++++++++++----- src/api/v1/auth/register.rs | 110 ++++++++++++--------- src/api/v1/auth/reset_password.rs | 49 ++++++---- src/api/v1/auth/revoke.rs | 37 +++---- src/api/v1/auth/verify_email.rs | 68 ++++++------- src/api/v1/channels/mod.rs | 28 ++++-- src/api/v1/channels/uuid/messages.rs | 48 ++++----- src/api/v1/channels/uuid/mod.rs | 110 ++++++++++----------- src/api/v1/guilds/mod.rs | 74 +++++++------- src/api/v1/guilds/uuid/channels.rs | 100 +++++++++---------- src/api/v1/guilds/uuid/icon.rs | 62 ------------ src/api/v1/guilds/uuid/invites/mod.rs | 69 +++++++------ src/api/v1/guilds/uuid/members.rs | 45 +++++---- src/api/v1/guilds/uuid/mod.rs | 121 ++++++++++++++++------- src/api/v1/guilds/uuid/roles/mod.rs | 76 +++++++-------- src/api/v1/guilds/uuid/roles/uuid.rs | 52 +++++----- src/api/v1/invites/id.rs | 54 ++++++----- src/api/v1/invites/mod.rs | 15 ++- src/api/v1/me/friends/mod.rs | 57 ++++++----- src/api/v1/me/friends/uuid.rs | 41 ++++---- src/api/v1/me/guilds.rs | 32 +++--- src/api/v1/me/mod.rs | 134 ++++++++++++++------------ src/api/v1/mod.rs | 24 +++-- src/api/v1/stats.rs | 21 ++-- src/api/v1/users/mod.rs | 49 ++++++---- src/api/v1/users/uuid.rs | 42 ++++---- src/api/versions.rs | 7 +- src/error.rs | 70 +++++++++----- src/main.rs | 84 ++++++++-------- src/objects/channel.rs | 67 +++++++------ src/objects/email_token.rs | 28 +++--- src/objects/guild.rs | 6 +- src/objects/me.rs | 84 +++++++++------- src/objects/member.rs | 38 +++++--- src/objects/message.rs | 6 +- src/objects/mod.rs | 31 ++++++ src/objects/password_reset_token.rs | 77 ++++++++------- src/objects/role.rs | 17 +++- src/objects/user.rs | 17 ++-- src/socket.rs | 26 +++++ src/utils.rs | 111 +++++---------------- 47 files changed, 1381 insertions(+), 1129 deletions(-) delete mode 100644 src/api/v1/guilds/uuid/icon.rs create mode 100644 src/socket.rs diff --git a/Cargo.toml b/Cargo.toml index c1c71bc..aef435f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,6 @@ thiserror = "2.0.12" # CLI clap = { version = "4.5", features = ["derive"] } log = "0.4" -simple_logger = "5.0.0" # async futures = "0.3" @@ -30,19 +29,21 @@ futures-util = "0.3.31" # Data (de)serialization serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -toml = "0.8" +toml = "0.9" +bytes = "1.10.1" +rmpv = { version = "1.3.0", features = ["with-serde"] } # File Storage bindet = "0.3.2" bunny-api-tokio = { version = "0.4", features = ["edge_storage"], default-features = false } # Web Server -actix-web = "4.11" -actix-cors = "0.7.1" -actix-ws = "0.3.0" -actix-multipart = "0.7.2" +axum = { version = "0.8.4", features = ["macros", "multipart"] } +tower-http = { version = "0.6.6", features = ["cors"] } +axum-extra = { version = "0.10.1", features = ["cookie", "typed-header"] } +socketioxide = { version = "0.17.2", features = ["state"] } url = { version = "2.5", features = ["serde"] } -tokio-tungstenite = { version = "0.27", features = ["native-tls", "url"] } +time = "0.3.41" # Database uuid = { version = "1.17", features = ["serde", "v7"] } @@ -60,5 +61,5 @@ regex = "1.11" random-string = "1.1" lettre = { version = "0.11", features = ["tokio1", "tokio1-native-tls"] } chrono = { version = "0.4.41", features = ["serde"] } - +tracing-subscriber = "0.3.19" diff --git a/src/api/mod.rs b/src/api/mod.rs index 6d83e02..e4c3f2e 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,13 +1,16 @@ //! `/api` Contains the entire API -use actix_web::Scope; -use actix_web::web; +use std::sync::Arc; + +use axum::{Router, routing::get}; + +use crate::AppState; mod v1; mod versions; -pub fn web(path: &str) -> Scope { - web::scope(path.trim_end_matches('/')) - .service(v1::web()) - .service(versions::get) +pub fn router() -> Router> { + Router::new() + .route("/versions", get(versions::versions)) + .nest("/v1", v1::router()) } diff --git a/src/api/v1/auth/devices.rs b/src/api/v1/auth/devices.rs index 532ad00..a3c12d1 100644 --- a/src/api/v1/auth/devices.rs +++ b/src/api/v1/auth/devices.rs @@ -1,16 +1,21 @@ //! `/api/v1/auth/devices` Returns list of logged in devices -use actix_web::{HttpRequest, HttpResponse, get, web}; +use std::sync::Arc; + +use axum::{Json, extract::State, http::StatusCode, response::IntoResponse}; +use axum_extra::{ + TypedHeader, + headers::{Authorization, authorization::Bearer}, +}; use diesel::{ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper}; use diesel_async::RunQueryDsl; use serde::Serialize; use crate::{ - Data, + AppState, api::v1::auth::check_access_token, error::Error, schema::refresh_tokens::{self, dsl}, - utils::get_auth_header, }; #[derive(Serialize, Selectable, Queryable)] @@ -18,7 +23,7 @@ use crate::{ #[diesel(check_for_backend(diesel::pg::Pg))] struct Device { device_name: String, - created_at: i64 + created_at: i64, } /// `GET /api/v1/auth/devices` Returns list of logged in devices @@ -35,18 +40,13 @@ struct Device { /// /// ]); /// ``` -#[get("/devices")] pub async fn get( - req: HttpRequest, - data: web::Data, -) -> Result { - let headers = req.headers(); + State(app_state): State>, + TypedHeader(auth): TypedHeader>, +) -> Result { + let mut conn = app_state.pool.get().await?; - let auth_header = get_auth_header(headers)?; - - let mut conn = data.pool.get().await?; - - let uuid = check_access_token(auth_header, &mut conn).await?; + let uuid = check_access_token(auth.token(), &mut conn).await?; let devices: Vec = dsl::refresh_tokens .filter(dsl::uuid.eq(uuid)) @@ -54,5 +54,5 @@ pub async fn get( .get_results(&mut conn) .await?; - Ok(HttpResponse::Ok().json(devices)) + Ok((StatusCode::OK, Json(devices))) } diff --git a/src/api/v1/auth/login.rs b/src/api/v1/auth/login.rs index 2faaeb4..2391fdf 100644 --- a/src/api/v1/auth/login.rs +++ b/src/api/v1/auth/login.rs @@ -1,39 +1,47 @@ -use std::time::{SystemTime, UNIX_EPOCH}; +use std::{ + sync::Arc, + time::{SystemTime, UNIX_EPOCH}, +}; -use actix_web::{HttpResponse, post, web}; use argon2::{PasswordHash, PasswordVerifier}; +use axum::{ + Json, + extract::State, + http::{HeaderValue, StatusCode}, + response::IntoResponse, +}; use diesel::{ExpressionMethods, QueryDsl, dsl::insert_into}; use diesel_async::RunQueryDsl; use serde::Deserialize; use crate::{ - Data, + AppState, error::Error, schema::*, - utils::{PASSWORD_REGEX, generate_token, new_refresh_token_cookie, user_uuid_from_identifier}, + utils::{ + PASSWORD_REGEX, generate_token, new_access_token_cookie, new_refresh_token_cookie, + user_uuid_from_identifier, + }, }; -use super::Response; - #[derive(Deserialize)] -struct LoginInformation { +pub struct LoginInformation { username: String, password: String, device_name: String, } -#[post("/login")] pub async fn response( - login_information: web::Json, - data: web::Data, -) -> Result { + State(app_state): State>, + Json(login_information): Json, +) -> Result { if !PASSWORD_REGEX.is_match(&login_information.password) { - return Ok(HttpResponse::Forbidden().json(r#"{ "password_hashed": false }"#)); + return Err(Error::BadRequest("Bad password".to_string())); } use users::dsl; - let mut conn = data.pool.get().await?; + let mut conn = app_state.pool.get().await?; let uuid = user_uuid_from_identifier(&mut conn, &login_information.username).await?; @@ -46,7 +54,7 @@ pub async fn response( let parsed_hash = PasswordHash::new(&database_password) .map_err(|e| Error::PasswordHashError(e.to_string()))?; - if data + if app_state .argon2 .verify_password(login_information.password.as_bytes(), &parsed_hash) .is_err() @@ -85,7 +93,21 @@ pub async fn response( .execute(&mut conn) .await?; - Ok(HttpResponse::Ok() - .cookie(new_refresh_token_cookie(&data.config, refresh_token)) - .json(Response { access_token })) + let mut response = StatusCode::OK.into_response(); + + response.headers_mut().insert( + "Set-Cookie", + HeaderValue::from_str( + &new_refresh_token_cookie(&app_state.config, refresh_token).to_string(), + )?, + ); + + response.headers_mut().insert( + "Set-Cookie2", + HeaderValue::from_str( + &new_access_token_cookie(&app_state.config, access_token).to_string(), + )?, + ); + + Ok(response) } diff --git a/src/api/v1/auth/logout.rs b/src/api/v1/auth/logout.rs index b805d91..6e5e98d 100644 --- a/src/api/v1/auth/logout.rs +++ b/src/api/v1/auth/logout.rs @@ -1,9 +1,16 @@ -use actix_web::{HttpRequest, HttpResponse, get, web}; +use std::sync::Arc; + +use axum::{ + extract::State, + http::{HeaderValue, StatusCode}, + response::IntoResponse, +}; +use axum_extra::extract::CookieJar; use diesel::{ExpressionMethods, delete}; use diesel_async::RunQueryDsl; use crate::{ - Data, + AppState, error::Error, schema::refresh_tokens::{self, dsl}, }; @@ -20,28 +27,49 @@ use crate::{ /// /// 401 Unauthorized (no refresh token found) /// -#[get("/logout")] -pub async fn res(req: HttpRequest, data: web::Data) -> Result { - let mut refresh_token_cookie = req.cookie("refresh_token").ok_or(Error::Unauthorized( - "request has no refresh token".to_string(), - ))?; +pub async fn res( + State(app_state): State>, + jar: CookieJar, +) -> Result { + let mut refresh_token_cookie = jar + .get("refresh_token") + .ok_or(Error::Unauthorized( + "request has no refresh token".to_string(), + ))? + .to_owned(); - let refresh_token = String::from(refresh_token_cookie.value()); + let access_token_cookie = jar.get("access_token"); - let mut conn = data.pool.get().await?; + let refresh_token = String::from(refresh_token_cookie.value_trimmed()); + + let mut conn = app_state.pool.get().await?; let deleted = delete(refresh_tokens::table) .filter(dsl::token.eq(refresh_token)) .execute(&mut conn) .await?; - refresh_token_cookie.make_removal(); + let mut response; if deleted == 0 { - return Ok(HttpResponse::NotFound() - .cookie(refresh_token_cookie) - .finish()); + response = StatusCode::NOT_FOUND.into_response(); + } else { + response = StatusCode::OK.into_response(); } - Ok(HttpResponse::Ok().cookie(refresh_token_cookie).finish()) + refresh_token_cookie.make_removal(); + response.headers_mut().append( + "Set-Cookie", + HeaderValue::from_str(&refresh_token_cookie.to_string())?, + ); + + if let Some(cookie) = access_token_cookie { + let mut cookie = cookie.clone(); + cookie.make_removal(); + response + .headers_mut() + .append("Set-Cookie2", HeaderValue::from_str(&cookie.to_string())?); + } + + Ok(response) } diff --git a/src/api/v1/auth/mod.rs b/src/api/v1/auth/mod.rs index 0e6b006..88be220 100644 --- a/src/api/v1/auth/mod.rs +++ b/src/api/v1/auth/mod.rs @@ -1,12 +1,17 @@ -use std::time::{SystemTime, UNIX_EPOCH}; +use std::{ + sync::Arc, + time::{SystemTime, UNIX_EPOCH}, +}; -use actix_web::{Scope, web}; +use axum::{ + Router, + routing::{delete, get, post}, +}; use diesel::{ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; -use serde::Serialize; use uuid::Uuid; -use crate::{Conn, error::Error, schema::access_tokens::dsl}; +use crate::{AppState, Conn, error::Error, schema::access_tokens::dsl}; mod devices; mod login; @@ -17,23 +22,18 @@ mod reset_password; mod revoke; mod verify_email; -#[derive(Serialize)] -struct Response { - access_token: String, -} - -pub fn web() -> Scope { - web::scope("/auth") - .service(register::res) - .service(login::response) - .service(logout::res) - .service(refresh::res) - .service(revoke::res) - .service(verify_email::get) - .service(verify_email::post) - .service(reset_password::get) - .service(reset_password::post) - .service(devices::get) +pub fn router() -> Router> { + Router::new() + .route("/register", post(register::post)) + .route("/login", post(login::response)) + .route("/logout", delete(logout::res)) + .route("/refresh", post(refresh::post)) + .route("/revoke", post(revoke::post)) + .route("/verify-email", get(verify_email::get)) + .route("/verify-email", post(verify_email::post)) + .route("/reset-password", get(reset_password::get)) + .route("/reset-password", post(reset_password::post)) + .route("/devices", get(devices::get)) } pub async fn check_access_token(access_token: &str, conn: &mut Conn) -> Result { diff --git a/src/api/v1/auth/refresh.rs b/src/api/v1/auth/refresh.rs index abd9a34..2a7e611 100644 --- a/src/api/v1/auth/refresh.rs +++ b/src/api/v1/auth/refresh.rs @@ -1,32 +1,45 @@ -use actix_web::{HttpRequest, HttpResponse, post, web}; +use axum::{ + extract::State, + http::{HeaderValue, StatusCode}, + response::IntoResponse, +}; +use axum_extra::extract::CookieJar; use diesel::{ExpressionMethods, QueryDsl, delete, update}; use diesel_async::RunQueryDsl; use log::error; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::{ + sync::Arc, + time::{SystemTime, UNIX_EPOCH}, +}; use crate::{ - Data, + AppState, error::Error, schema::{ access_tokens::{self, dsl}, refresh_tokens::{self, dsl as rdsl}, }, - utils::{generate_token, new_refresh_token_cookie}, + utils::{generate_token, new_access_token_cookie, new_refresh_token_cookie}, }; -use super::Response; +pub async fn post( + State(app_state): State>, + jar: CookieJar, +) -> Result { + let mut refresh_token_cookie = jar + .get("refresh_token") + .ok_or(Error::Unauthorized( + "request has no refresh token".to_string(), + ))? + .to_owned(); -#[post("/refresh")] -pub async fn res(req: HttpRequest, data: web::Data) -> Result { - let mut refresh_token_cookie = req.cookie("refresh_token").ok_or(Error::Unauthorized( - "request has no refresh token".to_string(), - ))?; + let access_token_cookie = jar.get("access_token"); - let mut refresh_token = String::from(refresh_token_cookie.value()); + let refresh_token = String::from(refresh_token_cookie.value_trimmed()); let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; - let mut conn = data.pool.get().await?; + let mut conn = app_state.pool.get().await?; if let Ok(created_at) = rdsl::refresh_tokens .filter(rdsl::token.eq(&refresh_token)) @@ -45,15 +58,29 @@ pub async fn res(req: HttpRequest, data: web::Data) -> Result 1987200 { let new_refresh_token = generate_token::<32>()?; @@ -67,7 +94,13 @@ pub async fn res(req: HttpRequest, data: web::Data) -> Result { - refresh_token = new_refresh_token; + response.headers_mut().append( + "Set-Cookie", + HeaderValue::from_str( + &new_refresh_token_cookie(&app_state.config, new_refresh_token) + .to_string(), + )?, + ); } Err(error) => { error!("{error}"); @@ -86,14 +119,40 @@ pub async fn res(req: HttpRequest, data: web::Data) -> Result, - data: web::Data, -) -> Result { - if !data.config.instance.registration { +pub async fn post( + State(app_state): State>, + Json(account_information): Json, +) -> Result { + if !app_state.config.instance.registration { return Err(Error::Forbidden( "registration is disabled on this instance".to_string(), )); @@ -78,36 +77,48 @@ pub async fn res( let uuid = Uuid::now_v7(); if !EMAIL_REGEX.is_match(&account_information.email) { - return Ok(HttpResponse::Forbidden().json(ResponseError { - email_valid: false, - ..Default::default() - })); + return Ok(( + StatusCode::FORBIDDEN, + Json(ResponseError { + email_valid: false, + ..Default::default() + }), + ) + .into_response()); } if !USERNAME_REGEX.is_match(&account_information.identifier) || account_information.identifier.len() < 3 || account_information.identifier.len() > 32 { - return Ok(HttpResponse::Forbidden().json(ResponseError { - gorb_id_valid: false, - ..Default::default() - })); + return Ok(( + StatusCode::FORBIDDEN, + Json(ResponseError { + gorb_id_valid: false, + ..Default::default() + }), + ) + .into_response()); } if !PASSWORD_REGEX.is_match(&account_information.password) { - return Ok(HttpResponse::Forbidden().json(ResponseError { - password_hashed: false, - ..Default::default() - })); + return Ok(( + StatusCode::FORBIDDEN, + Json(ResponseError { + password_strength: false, + ..Default::default() + }), + ) + .into_response()); } let salt = SaltString::generate(&mut OsRng); - if let Ok(hashed_password) = data + if let Ok(hashed_password) = app_state .argon2 .hash_password(account_information.password.as_bytes(), &salt) { - let mut conn = data.pool.get().await?; + let mut conn = app_state.pool.get().await?; // TODO: Check security of this implementation insert_into(users::table) @@ -145,14 +156,27 @@ pub async fn res( .execute(&mut conn) .await?; - if let Some(initial_guild) = data.config.instance.initial_guild { - Member::new(&data, uuid, initial_guild).await?; + if let Some(initial_guild) = app_state.config.instance.initial_guild { + Member::new(&app_state, uuid, initial_guild).await?; } - return Ok(HttpResponse::Ok() - .cookie(new_refresh_token_cookie(&data.config, refresh_token)) - .json(Response { access_token })); + let mut response = StatusCode::OK.into_response(); + + response.headers_mut().append( + "Set-Cookie", + HeaderValue::from_str( + &new_refresh_token_cookie(&app_state.config, refresh_token).to_string(), + )?, + ); + response.headers_mut().append( + "Set-Cookie2", + HeaderValue::from_str( + &new_access_token_cookie(&app_state.config, access_token).to_string(), + )?, + ); + + return Ok(response); } - Ok(HttpResponse::InternalServerError().finish()) + Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response()) } diff --git a/src/api/v1/auth/reset_password.rs b/src/api/v1/auth/reset_password.rs index 9a4497f..bac465c 100644 --- a/src/api/v1/auth/reset_password.rs +++ b/src/api/v1/auth/reset_password.rs @@ -1,13 +1,20 @@ //! `/api/v1/auth/reset-password` Endpoints for resetting user password -use actix_web::{HttpResponse, get, post, web}; +use std::sync::Arc; + +use axum::{ + Json, + extract::{Query, State}, + http::StatusCode, + response::IntoResponse, +}; use chrono::{Duration, Utc}; use serde::Deserialize; -use crate::{Data, error::Error, objects::PasswordResetToken}; +use crate::{AppState, error::Error, objects::PasswordResetToken}; #[derive(Deserialize)] -struct Query { +pub struct QueryParams { identifier: String, } @@ -20,17 +27,22 @@ struct Query { /// /// ### Responses /// 200 Email sent +/// /// 429 Too Many Requests +/// /// 404 Not found +/// /// 400 Bad request /// -#[get("/reset-password")] -pub async fn get(query: web::Query, data: web::Data) -> Result { +pub async fn get( + State(app_state): State>, + query: Query, +) -> Result { if let Ok(password_reset_token) = - PasswordResetToken::get_with_identifier(&data, query.identifier.clone()).await + PasswordResetToken::get_with_identifier(&app_state, query.identifier.clone()).await { if Utc::now().signed_duration_since(password_reset_token.created_at) > Duration::hours(1) { - password_reset_token.delete(&data).await?; + password_reset_token.delete(&app_state).await?; } else { return Err(Error::TooManyRequests( "Please allow 1 hour before sending a new email".to_string(), @@ -38,13 +50,13 @@ pub async fn get(query: web::Query, data: web::Data) -> Result, - data: web::Data, -) -> Result { - let password_reset_token = PasswordResetToken::get(&data, reset_password.token.clone()).await?; + State(app_state): State>, + reset_password: Json, +) -> Result { + let password_reset_token = + PasswordResetToken::get(&app_state, reset_password.token.clone()).await?; password_reset_token - .set_password(&data, reset_password.password.clone()) + .set_password(&app_state, reset_password.password.clone()) .await?; - Ok(HttpResponse::Ok().finish()) + Ok(StatusCode::OK) } diff --git a/src/api/v1/auth/revoke.rs b/src/api/v1/auth/revoke.rs index 2e95884..50aa6d2 100644 --- a/src/api/v1/auth/revoke.rs +++ b/src/api/v1/auth/revoke.rs @@ -1,38 +1,39 @@ -use actix_web::{HttpRequest, HttpResponse, post, web}; +use std::sync::Arc; + use argon2::{PasswordHash, PasswordVerifier}; +use axum::{Json, extract::State, http::StatusCode, response::IntoResponse}; +use axum_extra::{ + TypedHeader, + headers::authorization::{Authorization, Bearer}, +}; use diesel::{ExpressionMethods, QueryDsl, delete}; use diesel_async::RunQueryDsl; use serde::Deserialize; use crate::{ - Data, + AppState, api::v1::auth::check_access_token, error::Error, schema::refresh_tokens::{self, dsl as rdsl}, schema::users::dsl as udsl, - utils::get_auth_header, }; #[derive(Deserialize)] -struct RevokeRequest { +pub struct RevokeRequest { password: String, device_name: String, } // TODO: Should maybe be a delete request? -#[post("/revoke")] -pub async fn res( - req: HttpRequest, - revoke_request: web::Json, - data: web::Data, -) -> Result { - let headers = req.headers(); +#[axum::debug_handler] +pub async fn post( + State(app_state): State>, + TypedHeader(auth): TypedHeader>, + Json(revoke_request): Json, +) -> Result { + let mut conn = app_state.pool.get().await?; - let auth_header = get_auth_header(headers)?; - - let mut conn = data.pool.get().await?; - - let uuid = check_access_token(auth_header, &mut conn).await?; + let uuid = check_access_token(auth.token(), &mut conn).await?; let database_password: String = udsl::users .filter(udsl::uuid.eq(uuid)) @@ -43,7 +44,7 @@ pub async fn res( let hashed_password = PasswordHash::new(&database_password) .map_err(|e| Error::PasswordHashError(e.to_string()))?; - if data + if app_state .argon2 .verify_password(revoke_request.password.as_bytes(), &hashed_password) .is_err() @@ -59,5 +60,5 @@ pub async fn res( .execute(&mut conn) .await?; - Ok(HttpResponse::Ok().finish()) + Ok(StatusCode::OK) } diff --git a/src/api/v1/auth/verify_email.rs b/src/api/v1/auth/verify_email.rs index 6b895aa..28aa1ab 100644 --- a/src/api/v1/auth/verify_email.rs +++ b/src/api/v1/auth/verify_email.rs @@ -1,19 +1,28 @@ //! `/api/v1/auth/verify-email` Endpoints for verifying user emails -use actix_web::{HttpRequest, HttpResponse, get, post, web}; +use std::sync::Arc; + +use axum::{ + extract::{Query, State}, + http::StatusCode, + response::IntoResponse, +}; +use axum_extra::{ + TypedHeader, + headers::{Authorization, authorization::Bearer}, +}; use chrono::{Duration, Utc}; use serde::Deserialize; use crate::{ - Data, + AppState, api::v1::auth::check_access_token, error::Error, objects::{EmailToken, Me}, - utils::get_auth_header, }; #[derive(Deserialize)] -struct Query { +pub struct QueryParams { token: String, } @@ -35,37 +44,32 @@ struct Query { /// /// 401 Unauthorized /// -#[get("/verify-email")] pub async fn get( - req: HttpRequest, - query: web::Query, - data: web::Data, -) -> Result { - let headers = req.headers(); + State(app_state): State>, + Query(query): Query, + TypedHeader(auth): TypedHeader>, +) -> Result { + let mut conn = app_state.pool.get().await?; - let auth_header = get_auth_header(headers)?; - - let mut conn = data.pool.get().await?; - - let uuid = check_access_token(auth_header, &mut conn).await?; + let uuid = check_access_token(auth.token(), &mut conn).await?; let me = Me::get(&mut conn, uuid).await?; if me.email_verified { - return Ok(HttpResponse::NoContent().finish()); + return Ok(StatusCode::NO_CONTENT); } - let email_token = EmailToken::get(&data, me.uuid).await?; + let email_token = EmailToken::get(&app_state, me.uuid).await?; if query.token != email_token.token { - return Ok(HttpResponse::Unauthorized().finish()); + return Ok(StatusCode::UNAUTHORIZED); } me.verify_email(&mut conn).await?; - email_token.delete(&data).await?; + email_token.delete(&app_state).await?; - Ok(HttpResponse::Ok().finish()) + Ok(StatusCode::OK) } /// `POST /api/v1/auth/verify-email` Sends user verification email @@ -81,25 +85,23 @@ pub async fn get( /// /// 401 Unauthorized /// -#[post("/verify-email")] -pub async fn post(req: HttpRequest, data: web::Data) -> Result { - let headers = req.headers(); +pub async fn post( + State(app_state): State>, + TypedHeader(auth): TypedHeader>, +) -> Result { + let mut conn = app_state.pool.get().await?; - let auth_header = get_auth_header(headers)?; - - let mut conn = data.pool.get().await?; - - let uuid = check_access_token(auth_header, &mut conn).await?; + let uuid = check_access_token(auth.token(), &mut conn).await?; let me = Me::get(&mut conn, uuid).await?; if me.email_verified { - return Ok(HttpResponse::NoContent().finish()); + return Ok(StatusCode::NO_CONTENT); } - if let Ok(email_token) = EmailToken::get(&data, me.uuid).await { + if let Ok(email_token) = EmailToken::get(&app_state, me.uuid).await { if Utc::now().signed_duration_since(email_token.created_at) > Duration::hours(1) { - email_token.delete(&data).await?; + email_token.delete(&app_state).await?; } else { return Err(Error::TooManyRequests( "Please allow 1 hour before sending a new email".to_string(), @@ -107,7 +109,7 @@ pub async fn post(req: HttpRequest, data: web::Data) -> Result Scope { - web::scope("/channels") - .service(uuid::get) - .service(uuid::delete) - .service(uuid::patch) - .service(uuid::messages::get) - .service(uuid::socket::ws) +pub fn router() -> Router> { + //let (layer, io) = SocketIo::new_layer(); + + //io.ns("/{uuid}/socket", uuid::socket::ws); + + Router::new() + .route("/{uuid}", get(uuid::get)) + .route("/{uuid}", delete(uuid::delete)) + .route("/{uuid}", patch(uuid::patch)) + .route("/{uuid}/messages", get(uuid::messages::get)) + //.layer(layer) } diff --git a/src/api/v1/channels/uuid/messages.rs b/src/api/v1/channels/uuid/messages.rs index 9fdea0b..8c12ee0 100644 --- a/src/api/v1/channels/uuid/messages.rs +++ b/src/api/v1/channels/uuid/messages.rs @@ -1,18 +1,29 @@ //! `/api/v1/channels/{uuid}/messages` Endpoints related to channel messages +use std::sync::Arc; + use crate::{ - Data, + AppState, api::v1::auth::check_access_token, error::Error, objects::{Channel, Member}, - utils::{get_auth_header, global_checks}, + utils::global_checks, }; use ::uuid::Uuid; -use actix_web::{HttpRequest, HttpResponse, get, web}; +use axum::{ + Json, + extract::{Path, Query, State}, + http::StatusCode, + response::IntoResponse, +}; +use axum_extra::{ + TypedHeader, + headers::{Authorization, authorization::Bearer}, +}; use serde::Deserialize; #[derive(Deserialize)] -struct MessageRequest { +pub struct MessageRequest { amount: i64, offset: i64, } @@ -47,32 +58,25 @@ struct MessageRequest { /// }); /// ``` /// -#[get("/{uuid}/messages")] pub async fn get( - req: HttpRequest, - path: web::Path<(Uuid,)>, - message_request: web::Query, - data: web::Data, -) -> Result { - let headers = req.headers(); + State(app_state): State>, + Path(channel_uuid): Path, + Query(message_request): Query, + TypedHeader(auth): TypedHeader>, +) -> Result { + let mut conn = app_state.pool.get().await?; - let auth_header = get_auth_header(headers)?; + let uuid = check_access_token(auth.token(), &mut conn).await?; - let channel_uuid = path.into_inner().0; + global_checks(&app_state, uuid).await?; - let mut conn = data.pool.get().await?; - - let uuid = check_access_token(auth_header, &mut conn).await?; - - global_checks(&data, uuid).await?; - - let channel = Channel::fetch_one(&data, channel_uuid).await?; + let channel = Channel::fetch_one(&app_state, channel_uuid).await?; Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; let messages = channel - .fetch_messages(&data, message_request.amount, message_request.offset) + .fetch_messages(&app_state, message_request.amount, message_request.offset) .await?; - Ok(HttpResponse::Ok().json(messages)) + Ok((StatusCode::OK, Json(messages))) } diff --git a/src/api/v1/channels/uuid/mod.rs b/src/api/v1/channels/uuid/mod.rs index fff2ef0..3ce91c3 100644 --- a/src/api/v1/channels/uuid/mod.rs +++ b/src/api/v1/channels/uuid/mod.rs @@ -1,77 +1,74 @@ //! `/api/v1/channels/{uuid}` Channel specific endpoints pub mod messages; -pub mod socket; +//pub mod socket; + +use std::sync::Arc; use crate::{ - Data, + AppState, api::v1::auth::check_access_token, error::Error, objects::{Channel, Member, Permissions}, - utils::{get_auth_header, global_checks}, + utils::global_checks, +}; +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use axum_extra::{ + TypedHeader, + headers::{Authorization, authorization::Bearer}, }; -use actix_web::{HttpRequest, HttpResponse, delete, get, patch, web}; use serde::Deserialize; use uuid::Uuid; -#[get("/{uuid}")] pub async fn get( - req: HttpRequest, - path: web::Path<(Uuid,)>, - data: web::Data, -) -> Result { - let headers = req.headers(); + State(app_state): State>, + Path(channel_uuid): Path, + TypedHeader(auth): TypedHeader>, +) -> Result { + let mut conn = app_state.pool.get().await?; - let auth_header = get_auth_header(headers)?; + let uuid = check_access_token(auth.token(), &mut conn).await?; - let channel_uuid = path.into_inner().0; + global_checks(&app_state, uuid).await?; - let mut conn = data.pool.get().await?; - - let uuid = check_access_token(auth_header, &mut conn).await?; - - global_checks(&data, uuid).await?; - - let channel = Channel::fetch_one(&data, channel_uuid).await?; + let channel = Channel::fetch_one(&app_state, channel_uuid).await?; Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; - Ok(HttpResponse::Ok().json(channel)) + Ok((StatusCode::OK, Json(channel))) } -#[delete("/{uuid}")] pub async fn delete( - req: HttpRequest, - path: web::Path<(Uuid,)>, - data: web::Data, -) -> Result { - let headers = req.headers(); + State(app_state): State>, + Path(channel_uuid): Path, + TypedHeader(auth): TypedHeader>, +) -> Result { + let mut conn = app_state.pool.get().await?; - let auth_header = get_auth_header(headers)?; + let uuid = check_access_token(auth.token(), &mut conn).await?; - let channel_uuid = path.into_inner().0; + global_checks(&app_state, uuid).await?; - let mut conn = data.pool.get().await?; - - let uuid = check_access_token(auth_header, &mut conn).await?; - - global_checks(&data, uuid).await?; - - let channel = Channel::fetch_one(&data, channel_uuid).await?; + let channel = Channel::fetch_one(&app_state, channel_uuid).await?; let member = Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; member - .check_permission(&data, Permissions::ManageChannel) + .check_permission(&app_state, Permissions::ManageChannel) .await?; - channel.delete(&data).await?; + channel.delete(&app_state).await?; - Ok(HttpResponse::Ok().finish()) + Ok(StatusCode::OK) } #[derive(Deserialize)] -struct NewInfo { +pub struct NewInfo { name: Option, description: Option, is_above: Option, @@ -108,48 +105,41 @@ struct NewInfo { /// }); /// ``` /// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps -#[patch("/{uuid}")] pub async fn patch( - req: HttpRequest, - path: web::Path<(Uuid,)>, - new_info: web::Json, - data: web::Data, -) -> Result { - let headers = req.headers(); + State(app_state): State>, + Path(channel_uuid): Path, + TypedHeader(auth): TypedHeader>, + Json(new_info): Json, +) -> Result { + let mut conn = app_state.pool.get().await?; - let auth_header = get_auth_header(headers)?; + let uuid = check_access_token(auth.token(), &mut conn).await?; - let channel_uuid = path.into_inner().0; + global_checks(&app_state, uuid).await?; - let mut conn = data.pool.get().await?; - - let uuid = check_access_token(auth_header, &mut conn).await?; - - global_checks(&data, uuid).await?; - - let mut channel = Channel::fetch_one(&data, channel_uuid).await?; + let mut channel = Channel::fetch_one(&app_state, channel_uuid).await?; let member = Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; member - .check_permission(&data, Permissions::ManageChannel) + .check_permission(&app_state, Permissions::ManageChannel) .await?; if let Some(new_name) = &new_info.name { - channel.set_name(&data, new_name.to_string()).await?; + channel.set_name(&app_state, new_name.to_string()).await?; } if let Some(new_description) = &new_info.description { channel - .set_description(&data, new_description.to_string()) + .set_description(&app_state, new_description.to_string()) .await?; } if let Some(new_is_above) = &new_info.is_above { channel - .set_description(&data, new_is_above.to_string()) + .set_description(&app_state, new_is_above.to_string()) .await?; } - Ok(HttpResponse::Ok().json(channel)) + Ok((StatusCode::OK, Json(channel))) } diff --git a/src/api/v1/guilds/mod.rs b/src/api/v1/guilds/mod.rs index ada5dc8..18a117f 100644 --- a/src/api/v1/guilds/mod.rs +++ b/src/api/v1/guilds/mod.rs @@ -1,28 +1,40 @@ //! `/api/v1/guilds` Guild related endpoints -use actix_web::{HttpRequest, HttpResponse, Scope, get, post, web}; +use std::sync::Arc; + +use axum::{ + Json, Router, + extract::State, + http::StatusCode, + response::IntoResponse, + routing::{get, post}, +}; +use axum_extra::{ + TypedHeader, + headers::{Authorization, authorization::Bearer}, +}; use serde::Deserialize; mod uuid; use crate::{ - Data, + AppState, api::v1::auth::check_access_token, error::Error, objects::{Guild, StartAmountQuery}, - utils::{get_auth_header, global_checks}, + utils::global_checks, }; #[derive(Deserialize)] -struct GuildInfo { +pub struct GuildInfo { name: String, } -pub fn web() -> Scope { - web::scope("/guilds") - .service(post) - .service(get) - .service(uuid::web()) +pub fn router() -> Router> { + Router::new() + .route("/", post(new)) + .route("/", get(get_guilds)) + .nest("/{uuid}", uuid::router()) } /// `POST /api/v1/guilds` Creates a new guild @@ -49,23 +61,18 @@ pub fn web() -> Scope { /// }); /// ``` /// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps -#[post("")] -pub async fn post( - req: HttpRequest, - guild_info: web::Json, - data: web::Data, -) -> Result { - let headers = req.headers(); +pub async fn new( + State(app_state): State>, + TypedHeader(auth): TypedHeader>, + Json(guild_info): Json, +) -> Result { + let mut conn = app_state.pool.get().await?; - let auth_header = get_auth_header(headers)?; - - let mut conn = data.pool.get().await?; - - let uuid = check_access_token(auth_header, &mut conn).await?; + let uuid = check_access_token(auth.token(), &mut conn).await?; let guild = Guild::new(&mut conn, guild_info.name.clone(), uuid).await?; - Ok(HttpResponse::Ok().json(guild)) + Ok((StatusCode::OK, Json(guild))) } /// `GET /api/v1/servers` Fetches all guilds @@ -115,25 +122,20 @@ pub async fn post( /// ]); /// ``` /// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps -#[get("")] -pub async fn get( - req: HttpRequest, - request_query: web::Query, - data: web::Data, -) -> Result { - let headers = req.headers(); - - let auth_header = get_auth_header(headers)?; - +pub async fn get_guilds( + State(app_state): State>, + TypedHeader(auth): TypedHeader>, + Json(request_query): Json, +) -> Result { let start = request_query.start.unwrap_or(0); let amount = request_query.amount.unwrap_or(10); - let uuid = check_access_token(auth_header, &mut data.pool.get().await?).await?; + let uuid = check_access_token(auth.token(), &mut app_state.pool.get().await?).await?; - global_checks(&data, uuid).await?; + global_checks(&app_state, uuid).await?; - let guilds = Guild::fetch_amount(&data.pool, start, amount).await?; + let guilds = Guild::fetch_amount(&app_state.pool, start, amount).await?; - Ok(HttpResponse::Ok().json(guilds)) + Ok((StatusCode::OK, Json(guilds))) } diff --git a/src/api/v1/guilds/uuid/channels.rs b/src/api/v1/guilds/uuid/channels.rs index b9f91cb..0104566 100644 --- a/src/api/v1/guilds/uuid/channels.rs +++ b/src/api/v1/guilds/uuid/channels.rs @@ -1,92 +1,92 @@ +use std::sync::Arc; + +use ::uuid::Uuid; +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use axum_extra::{ + TypedHeader, + headers::{Authorization, authorization::Bearer}, +}; +use serde::Deserialize; + use crate::{ - Data, + AppState, api::v1::auth::check_access_token, error::Error, objects::{Channel, Member, Permissions}, - utils::{get_auth_header, global_checks, order_by_is_above}, + utils::{global_checks, order_by_is_above}, }; -use ::uuid::Uuid; -use actix_web::{HttpRequest, HttpResponse, get, post, web}; -use serde::Deserialize; #[derive(Deserialize)] -struct ChannelInfo { +pub struct ChannelInfo { name: String, description: Option, } -#[get("{uuid}/channels")] pub async fn get( - req: HttpRequest, - path: web::Path<(Uuid,)>, - data: web::Data, -) -> Result { - let headers = req.headers(); + State(app_state): State>, + Path(guild_uuid): Path, + TypedHeader(auth): TypedHeader>, +) -> Result { + let mut conn = app_state.pool.get().await?; - let auth_header = get_auth_header(headers)?; + let uuid = check_access_token(auth.token(), &mut conn).await?; - let guild_uuid = path.into_inner().0; - - let mut conn = data.pool.get().await?; - - let uuid = check_access_token(auth_header, &mut conn).await?; - - global_checks(&data, uuid).await?; + global_checks(&app_state, uuid).await?; Member::check_membership(&mut conn, uuid, guild_uuid).await?; - if let Ok(cache_hit) = data.get_cache_key(format!("{guild_uuid}_channels")).await { - return Ok(HttpResponse::Ok() - .content_type("application/json") - .body(cache_hit)); + if let Ok(cache_hit) = app_state + .get_cache_key(format!("{guild_uuid}_channels")) + .await + { + return Ok((StatusCode::OK, Json(cache_hit)).into_response()); } - let channels = Channel::fetch_all(&data.pool, guild_uuid).await?; + let channels = Channel::fetch_all(&app_state.pool, guild_uuid).await?; let channels_ordered = order_by_is_above(channels).await?; - data.set_cache_key( - format!("{guild_uuid}_channels"), - channels_ordered.clone(), - 1800, - ) - .await?; + app_state + .set_cache_key( + format!("{guild_uuid}_channels"), + channels_ordered.clone(), + 1800, + ) + .await?; - Ok(HttpResponse::Ok().json(channels_ordered)) + Ok((StatusCode::OK, Json(channels_ordered)).into_response()) } -#[post("{uuid}/channels")] pub async fn create( - req: HttpRequest, - channel_info: web::Json, - path: web::Path<(Uuid,)>, - data: web::Data, -) -> Result { - let headers = req.headers(); + State(app_state): State>, + Path(guild_uuid): Path, + TypedHeader(auth): TypedHeader>, + Json(channel_info): Json, +) -> Result { + let mut conn = app_state.pool.get().await?; - let auth_header = get_auth_header(headers)?; + let uuid = check_access_token(auth.token(), &mut conn).await?; - let guild_uuid = path.into_inner().0; - - let mut conn = data.pool.get().await?; - - let uuid = check_access_token(auth_header, &mut conn).await?; - - global_checks(&data, uuid).await?; + global_checks(&app_state, uuid).await?; let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; member - .check_permission(&data, Permissions::ManageChannel) + .check_permission(&app_state, Permissions::ManageChannel) .await?; let channel = Channel::new( - data.clone(), + &app_state, guild_uuid, channel_info.name.clone(), channel_info.description.clone(), ) .await?; - Ok(HttpResponse::Ok().json(channel)) + Ok((StatusCode::OK, Json(channel))) } diff --git a/src/api/v1/guilds/uuid/icon.rs b/src/api/v1/guilds/uuid/icon.rs deleted file mode 100644 index 600ccba..0000000 --- a/src/api/v1/guilds/uuid/icon.rs +++ /dev/null @@ -1,62 +0,0 @@ -//! `/api/v1/guilds/{uuid}/icon` icon related endpoints, will probably be replaced by a multipart post to above endpoint - -use actix_web::{HttpRequest, HttpResponse, put, web}; -use futures_util::StreamExt as _; -use uuid::Uuid; - -use crate::{ - Data, - api::v1::auth::check_access_token, - error::Error, - objects::{Guild, Member, Permissions}, - utils::{get_auth_header, global_checks}, -}; - -/// `PUT /api/v1/guilds/{uuid}/icon` Icon upload -/// -/// requires auth: no -/// -/// put request expects a file and nothing else -#[put("{uuid}/icon")] -pub async fn upload( - req: HttpRequest, - path: web::Path<(Uuid,)>, - mut payload: web::Payload, - data: web::Data, -) -> Result { - let headers = req.headers(); - - let auth_header = get_auth_header(headers)?; - - let guild_uuid = path.into_inner().0; - - let mut conn = data.pool.get().await?; - - let uuid = check_access_token(auth_header, &mut conn).await?; - - global_checks(&data, uuid).await?; - - let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; - - member - .check_permission(&data, Permissions::ManageGuild) - .await?; - - let mut guild = Guild::fetch_one(&mut conn, guild_uuid).await?; - - let mut bytes = web::BytesMut::new(); - while let Some(item) = payload.next().await { - bytes.extend_from_slice(&item?); - } - - guild - .set_icon( - &data.bunny_storage, - &mut conn, - data.config.bunny.cdn_url.clone(), - bytes, - ) - .await?; - - Ok(HttpResponse::Ok().finish()) -} diff --git a/src/api/v1/guilds/uuid/invites/mod.rs b/src/api/v1/guilds/uuid/invites/mod.rs index f1c62bc..7703cf7 100644 --- a/src/api/v1/guilds/uuid/invites/mod.rs +++ b/src/api/v1/guilds/uuid/invites/mod.rs @@ -1,37 +1,41 @@ -use actix_web::{HttpRequest, HttpResponse, get, post, web}; +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use axum_extra::{ + TypedHeader, + headers::{Authorization, authorization::Bearer}, +}; use serde::Deserialize; use uuid::Uuid; use crate::{ - Data, + AppState, api::v1::auth::check_access_token, error::Error, objects::{Guild, Member, Permissions}, - utils::{get_auth_header, global_checks}, + utils::global_checks, }; #[derive(Deserialize)] -struct InviteRequest { +pub struct InviteRequest { custom_id: Option, } -#[get("{uuid}/invites")] pub async fn get( - req: HttpRequest, - path: web::Path<(Uuid,)>, - data: web::Data, -) -> Result { - let headers = req.headers(); + State(app_state): State>, + Path(guild_uuid): Path, + TypedHeader(auth): TypedHeader>, +) -> Result { + let mut conn = app_state.pool.get().await?; - let auth_header = get_auth_header(headers)?; + let uuid = check_access_token(auth.token(), &mut conn).await?; - let guild_uuid = path.into_inner().0; - - let mut conn = data.pool.get().await?; - - let uuid = check_access_token(auth_header, &mut conn).await?; - - global_checks(&data, uuid).await?; + global_checks(&app_state, uuid).await?; Member::check_membership(&mut conn, uuid, guild_uuid).await?; @@ -39,32 +43,25 @@ pub async fn get( let invites = guild.get_invites(&mut conn).await?; - Ok(HttpResponse::Ok().json(invites)) + Ok((StatusCode::OK, Json(invites))) } -#[post("{uuid}/invites")] pub async fn create( - req: HttpRequest, - path: web::Path<(Uuid,)>, - invite_request: web::Json, - data: web::Data, -) -> Result { - let headers = req.headers(); + State(app_state): State>, + Path(guild_uuid): Path, + TypedHeader(auth): TypedHeader>, + Json(invite_request): Json, +) -> Result { + let mut conn = app_state.pool.get().await?; - let auth_header = get_auth_header(headers)?; + let uuid = check_access_token(auth.token(), &mut conn).await?; - let guild_uuid = path.into_inner().0; - - let mut conn = data.pool.get().await?; - - let uuid = check_access_token(auth_header, &mut conn).await?; - - global_checks(&data, uuid).await?; + global_checks(&app_state, uuid).await?; let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; member - .check_permission(&data, Permissions::CreateInvite) + .check_permission(&app_state, Permissions::CreateInvite) .await?; let guild = Guild::fetch_one(&mut conn, guild_uuid).await?; @@ -73,5 +70,5 @@ pub async fn create( .create_invite(&mut conn, uuid, invite_request.custom_id.clone()) .await?; - Ok(HttpResponse::Ok().json(invite)) + Ok((StatusCode::OK, Json(invite))) } diff --git a/src/api/v1/guilds/uuid/members.rs b/src/api/v1/guilds/uuid/members.rs index 0afc2c5..bd2f853 100644 --- a/src/api/v1/guilds/uuid/members.rs +++ b/src/api/v1/guilds/uuid/members.rs @@ -1,36 +1,41 @@ +use std::sync::Arc; + +use ::uuid::Uuid; +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use axum_extra::{ + TypedHeader, + headers::{Authorization, authorization::Bearer}, +}; + use crate::{ - Data, + AppState, api::v1::auth::check_access_token, error::Error, objects::{Me, Member}, - utils::{get_auth_header, global_checks}, + utils::global_checks, }; -use ::uuid::Uuid; -use actix_web::{HttpRequest, HttpResponse, get, web}; -#[get("{uuid}/members")] pub async fn get( - req: HttpRequest, - path: web::Path<(Uuid,)>, - data: web::Data, -) -> Result { - let headers = req.headers(); + State(app_state): State>, + Path(guild_uuid): Path, + TypedHeader(auth): TypedHeader>, +) -> Result { + let mut conn = app_state.pool.get().await?; - let auth_header = get_auth_header(headers)?; + let uuid = check_access_token(auth.token(), &mut conn).await?; - let guild_uuid = path.into_inner().0; - - let mut conn = data.pool.get().await?; - - let uuid = check_access_token(auth_header, &mut conn).await?; - - global_checks(&data, uuid).await?; + global_checks(&app_state, uuid).await?; Member::check_membership(&mut conn, uuid, guild_uuid).await?; let me = Me::get(&mut conn, uuid).await?; - let members = Member::fetch_all(&data, &me, guild_uuid).await?; + let members = Member::fetch_all(&app_state, &me, guild_uuid).await?; - Ok(HttpResponse::Ok().json(members)) + Ok((StatusCode::OK, Json(members))) } diff --git a/src/api/v1/guilds/uuid/mod.rs b/src/api/v1/guilds/uuid/mod.rs index 4c88d7a..0a27123 100644 --- a/src/api/v1/guilds/uuid/mod.rs +++ b/src/api/v1/guilds/uuid/mod.rs @@ -1,40 +1,51 @@ //! `/api/v1/guilds/{uuid}` Specific server endpoints -use actix_web::{HttpRequest, HttpResponse, Scope, get, web}; +use std::sync::Arc; + +use axum::{ + Json, Router, + extract::{Multipart, Path, State}, + http::StatusCode, + response::IntoResponse, + routing::{get, patch, post}, +}; +use axum_extra::{ + TypedHeader, + headers::{Authorization, authorization::Bearer}, +}; +use bytes::Bytes; use uuid::Uuid; mod channels; -mod icon; mod invites; mod members; mod roles; use crate::{ - Data, + AppState, api::v1::auth::check_access_token, error::Error, - objects::{Guild, Member}, - utils::{get_auth_header, global_checks}, + objects::{Guild, Member, Permissions}, + utils::global_checks, }; -pub fn web() -> Scope { - web::scope("") +pub fn router() -> Router> { + Router::new() // Servers - .service(get) + .route("/", get(get_guild)) + .route("/", patch(edit)) // Channels - .service(channels::get) - .service(channels::create) + .route("/channels", get(channels::get)) + .route("/channels", post(channels::create)) // Roles - .service(roles::get) - .service(roles::create) - .service(roles::uuid::get) + .route("/roles", get(roles::get)) + .route("/roles", post(roles::create)) + .route("/roles/{role_uuid}", get(roles::uuid::get)) // Invites - .service(invites::get) - .service(invites::create) - // Icon - .service(icon::upload) + .route("/invites", get(invites::get)) + .route("/invites", post(invites::create)) // Members - .service(members::get) + .route("/members", get(members::get)) } /// `GET /api/v1/guilds/{uuid}` DESCRIPTION @@ -70,27 +81,69 @@ pub fn web() -> Scope { /// "member_count": 20 /// }); /// ``` -#[get("/{uuid}")] -pub async fn get( - req: HttpRequest, - path: web::Path<(Uuid,)>, - data: web::Data, -) -> Result { - let headers = req.headers(); +pub async fn get_guild( + State(app_state): State>, + Path(guild_uuid): Path, + TypedHeader(auth): TypedHeader>, +) -> Result { + let mut conn = app_state.pool.get().await?; - let auth_header = get_auth_header(headers)?; + let uuid = check_access_token(auth.token(), &mut conn).await?; - let guild_uuid = path.into_inner().0; - - let mut conn = data.pool.get().await?; - - let uuid = check_access_token(auth_header, &mut conn).await?; - - global_checks(&data, uuid).await?; + global_checks(&app_state, uuid).await?; Member::check_membership(&mut conn, uuid, guild_uuid).await?; let guild = Guild::fetch_one(&mut conn, guild_uuid).await?; - Ok(HttpResponse::Ok().json(guild)) + Ok((StatusCode::OK, Json(guild))) +} + +/// `PATCH /api/v1/guilds/{uuid}` change guild settings +/// +/// requires auth: yes +pub async fn edit( + State(app_state): State>, + Path(guild_uuid): Path, + TypedHeader(auth): TypedHeader>, + mut multipart: Multipart, +) -> Result { + let mut conn = app_state.pool.get().await?; + + let uuid = check_access_token(auth.token(), &mut conn).await?; + + global_checks(&app_state, uuid).await?; + + let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; + + member + .check_permission(&app_state, Permissions::ManageGuild) + .await?; + + let mut guild = Guild::fetch_one(&mut conn, guild_uuid).await?; + + let mut icon: Option = None; + + while let Some(field) = multipart.next_field().await.unwrap() { + let name = field + .name() + .ok_or(Error::BadRequest("Field has no name".to_string()))?; + + if name == "icon" { + icon = Some(field.bytes().await?); + } + } + + if let Some(icon) = icon { + guild + .set_icon( + &app_state.bunny_storage, + &mut conn, + app_state.config.bunny.cdn_url.clone(), + icon, + ) + .await?; + } + + Ok(StatusCode::OK) } diff --git a/src/api/v1/guilds/uuid/roles/mod.rs b/src/api/v1/guilds/uuid/roles/mod.rs index 0fcc5b3..12960c2 100644 --- a/src/api/v1/guilds/uuid/roles/mod.rs +++ b/src/api/v1/guilds/uuid/roles/mod.rs @@ -1,82 +1,78 @@ +use std::sync::Arc; + use ::uuid::Uuid; -use actix_web::{HttpRequest, HttpResponse, get, post, web}; +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use axum_extra::{ + TypedHeader, + headers::{Authorization, authorization::Bearer}, +}; use serde::Deserialize; use crate::{ - Data, + AppState, api::v1::auth::check_access_token, error::Error, objects::{Member, Permissions, Role}, - utils::{get_auth_header, global_checks, order_by_is_above}, + utils::{global_checks, order_by_is_above}, }; pub mod uuid; #[derive(Deserialize)] -struct RoleInfo { +pub struct RoleInfo { name: String, } -#[get("{uuid}/roles")] pub async fn get( - req: HttpRequest, - path: web::Path<(Uuid,)>, - data: web::Data, -) -> Result { - let headers = req.headers(); + State(app_state): State>, + Path(guild_uuid): Path, + TypedHeader(auth): TypedHeader>, +) -> Result { + let mut conn = app_state.pool.get().await?; - let auth_header = get_auth_header(headers)?; - - let guild_uuid = path.into_inner().0; - - let mut conn = data.pool.get().await?; - - let uuid = check_access_token(auth_header, &mut conn).await?; + let uuid = check_access_token(auth.token(), &mut conn).await?; Member::check_membership(&mut conn, uuid, guild_uuid).await?; - if let Ok(cache_hit) = data.get_cache_key(format!("{guild_uuid}_roles")).await { - return Ok(HttpResponse::Ok() - .content_type("application/json") - .body(cache_hit)); + if let Ok(cache_hit) = app_state.get_cache_key(format!("{guild_uuid}_roles")).await { + return Ok((StatusCode::OK, Json(cache_hit)).into_response()); } let roles = Role::fetch_all(&mut conn, guild_uuid).await?; let roles_ordered = order_by_is_above(roles).await?; - data.set_cache_key(format!("{guild_uuid}_roles"), roles_ordered.clone(), 1800) + app_state + .set_cache_key(format!("{guild_uuid}_roles"), roles_ordered.clone(), 1800) .await?; - Ok(HttpResponse::Ok().json(roles_ordered)) + Ok((StatusCode::OK, Json(roles_ordered)).into_response()) } -#[post("{uuid}/roles")] pub async fn create( - req: HttpRequest, - role_info: web::Json, - path: web::Path<(Uuid,)>, - data: web::Data, -) -> Result { - let headers = req.headers(); + State(app_state): State>, + Path(guild_uuid): Path, + TypedHeader(auth): TypedHeader>, + Json(role_info): Json, +) -> Result { + let mut conn = app_state.pool.get().await?; - let auth_header = get_auth_header(headers)?; + let uuid = check_access_token(auth.token(), &mut conn).await?; - let guild_uuid = path.into_inner().0; - - let mut conn = data.pool.get().await?; - - let uuid = check_access_token(auth_header, &mut conn).await?; - - global_checks(&data, uuid).await?; + global_checks(&app_state, uuid).await?; let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; member - .check_permission(&data, Permissions::ManageRole) + .check_permission(&app_state, Permissions::ManageRole) .await?; let role = Role::new(&mut conn, guild_uuid, role_info.name.clone()).await?; - Ok(HttpResponse::Ok().json(role)) + Ok((StatusCode::OK, Json(role)).into_response()) } diff --git a/src/api/v1/guilds/uuid/roles/uuid.rs b/src/api/v1/guilds/uuid/roles/uuid.rs index bd747d8..a62a5b4 100644 --- a/src/api/v1/guilds/uuid/roles/uuid.rs +++ b/src/api/v1/guilds/uuid/roles/uuid.rs @@ -1,43 +1,47 @@ +use std::sync::Arc; + +use ::uuid::Uuid; +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use axum_extra::{ + TypedHeader, + headers::{Authorization, authorization::Bearer}, +}; + use crate::{ - Data, + AppState, api::v1::auth::check_access_token, error::Error, objects::{Member, Role}, - utils::{get_auth_header, global_checks}, + utils::global_checks, }; -use ::uuid::Uuid; -use actix_web::{HttpRequest, HttpResponse, get, web}; -#[get("{uuid}/roles/{role_uuid}")] pub async fn get( - req: HttpRequest, - path: web::Path<(Uuid, Uuid)>, - data: web::Data, -) -> Result { - let headers = req.headers(); + State(app_state): State>, + Path((guild_uuid, role_uuid)): Path<(Uuid, Uuid)>, + TypedHeader(auth): TypedHeader>, +) -> Result { + let mut conn = app_state.pool.get().await?; - let auth_header = get_auth_header(headers)?; + let uuid = check_access_token(auth.token(), &mut conn).await?; - let (guild_uuid, role_uuid) = path.into_inner(); - - let mut conn = data.pool.get().await?; - - let uuid = check_access_token(auth_header, &mut conn).await?; - - global_checks(&data, uuid).await?; + global_checks(&app_state, uuid).await?; Member::check_membership(&mut conn, uuid, guild_uuid).await?; - if let Ok(cache_hit) = data.get_cache_key(format!("{role_uuid}")).await { - return Ok(HttpResponse::Ok() - .content_type("application/json") - .body(cache_hit)); + if let Ok(cache_hit) = app_state.get_cache_key(format!("{role_uuid}")).await { + return Ok((StatusCode::OK, Json(cache_hit)).into_response()); } let role = Role::fetch_one(&mut conn, role_uuid).await?; - data.set_cache_key(format!("{role_uuid}"), role.clone(), 60) + app_state + .set_cache_key(format!("{role_uuid}"), role.clone(), 60) .await?; - Ok(HttpResponse::Ok().json(role)) + Ok((StatusCode::OK, Json(role)).into_response()) } diff --git a/src/api/v1/invites/id.rs b/src/api/v1/invites/id.rs index 22e2868..b832557 100644 --- a/src/api/v1/invites/id.rs +++ b/src/api/v1/invites/id.rs @@ -1,49 +1,53 @@ -use actix_web::{HttpRequest, HttpResponse, get, post, web}; +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use axum_extra::{ + TypedHeader, + headers::{Authorization, authorization::Bearer}, +}; use crate::{ - Data, + AppState, api::v1::auth::check_access_token, error::Error, objects::{Guild, Invite, Member}, - utils::{get_auth_header, global_checks}, + utils::global_checks, }; -#[get("{id}")] -pub async fn get(path: web::Path<(String,)>, data: web::Data) -> Result { - let mut conn = data.pool.get().await?; - - let invite_id = path.into_inner().0; +pub async fn get( + State(app_state): State>, + Path(invite_id): Path, +) -> Result { + let mut conn = app_state.pool.get().await?; let invite = Invite::fetch_one(&mut conn, invite_id).await?; let guild = Guild::fetch_one(&mut conn, invite.guild_uuid).await?; - Ok(HttpResponse::Ok().json(guild)) + Ok((StatusCode::OK, Json(guild))) } -#[post("{id}")] pub async fn join( - req: HttpRequest, - path: web::Path<(String,)>, - data: web::Data, -) -> Result { - let headers = req.headers(); + State(app_state): State>, + Path(invite_id): Path, + TypedHeader(auth): TypedHeader>, +) -> Result { + let mut conn = app_state.pool.get().await?; - let auth_header = get_auth_header(headers)?; + let uuid = check_access_token(auth.token(), &mut conn).await?; - let invite_id = path.into_inner().0; - - let mut conn = data.pool.get().await?; - - let uuid = check_access_token(auth_header, &mut conn).await?; - - global_checks(&data, uuid).await?; + global_checks(&app_state, uuid).await?; let invite = Invite::fetch_one(&mut conn, invite_id).await?; let guild = Guild::fetch_one(&mut conn, invite.guild_uuid).await?; - Member::new(&data, uuid, guild.uuid).await?; + Member::new(&app_state, uuid, guild.uuid).await?; - Ok(HttpResponse::Ok().json(guild)) + Ok((StatusCode::OK, Json(guild))) } diff --git a/src/api/v1/invites/mod.rs b/src/api/v1/invites/mod.rs index 3714a83..50fb707 100644 --- a/src/api/v1/invites/mod.rs +++ b/src/api/v1/invites/mod.rs @@ -1,7 +1,16 @@ -use actix_web::{Scope, web}; +use std::sync::Arc; + +use axum::{ + Router, + routing::{get, post}, +}; + +use crate::AppState; mod id; -pub fn web() -> Scope { - web::scope("/invites").service(id::get).service(id::join) +pub fn router() -> Router> { + Router::new() + .route("/{id}", get(id::get)) + .route("/{id}", post(id::join)) } diff --git a/src/api/v1/me/friends/mod.rs b/src/api/v1/me/friends/mod.rs index 8de0a5d..8a7851c 100644 --- a/src/api/v1/me/friends/mod.rs +++ b/src/api/v1/me/friends/mod.rs @@ -1,38 +1,42 @@ -use actix_web::{HttpRequest, HttpResponse, get, post, web}; +use std::sync::Arc; + +use axum::{Json, extract::State, http::StatusCode, response::IntoResponse}; +use axum_extra::{ + TypedHeader, + headers::{Authorization, authorization::Bearer}, +}; use serde::Deserialize; pub mod uuid; use crate::{ - Data, + AppState, api::v1::auth::check_access_token, error::Error, objects::Me, - utils::{get_auth_header, global_checks, user_uuid_from_username} + utils::{global_checks, user_uuid_from_username}, }; /// Returns a list of users that are your friends -#[get("/friends")] -pub async fn get(req: HttpRequest, data: web::Data) -> Result { - let headers = req.headers(); +pub async fn get( + State(app_state): State>, + TypedHeader(auth): TypedHeader>, +) -> Result { + let mut conn = app_state.pool.get().await?; - let auth_header = get_auth_header(headers)?; + let uuid = check_access_token(auth.token(), &mut conn).await?; - let mut conn = data.pool.get().await?; - - let uuid = check_access_token(auth_header, &mut conn).await?; - - global_checks(&data, uuid).await?; + global_checks(&app_state, uuid).await?; let me = Me::get(&mut conn, uuid).await?; - let friends = me.get_friends(&data).await?; + let friends = me.get_friends(&app_state).await?; - Ok(HttpResponse::Ok().json(friends)) + Ok((StatusCode::OK, Json(friends))) } #[derive(Deserialize)] -struct UserReq { +pub struct UserReq { username: String, } @@ -55,26 +59,21 @@ struct UserReq { /// /// 400 Bad Request (usually means users are already friends) /// -#[post("/friends")] pub async fn post( - req: HttpRequest, - json: web::Json, - data: web::Data, -) -> Result { - let headers = req.headers(); + State(app_state): State>, + TypedHeader(auth): TypedHeader>, + Json(user_request): Json, +) -> Result { + let mut conn = app_state.pool.get().await?; - let auth_header = get_auth_header(headers)?; + let uuid = check_access_token(auth.token(), &mut conn).await?; - let mut conn = data.pool.get().await?; - - let uuid = check_access_token(auth_header, &mut conn).await?; - - global_checks(&data, uuid).await?; + global_checks(&app_state, uuid).await?; let me = Me::get(&mut conn, uuid).await?; - let target_uuid = user_uuid_from_username(&mut conn, &json.username).await?; + let target_uuid = user_uuid_from_username(&mut conn, &user_request.username).await?; me.add_friend(&mut conn, target_uuid).await?; - Ok(HttpResponse::Ok().finish()) + Ok(StatusCode::OK) } diff --git a/src/api/v1/me/friends/uuid.rs b/src/api/v1/me/friends/uuid.rs index 34bfeff..8d40f26 100644 --- a/src/api/v1/me/friends/uuid.rs +++ b/src/api/v1/me/friends/uuid.rs @@ -1,33 +1,34 @@ -use actix_web::{HttpRequest, HttpResponse, delete, web}; +use std::sync::Arc; + +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use axum_extra::{ + TypedHeader, + headers::{Authorization, authorization::Bearer}, +}; use uuid::Uuid; use crate::{ - Data, - api::v1::auth::check_access_token, - error::Error, - objects::Me, - utils::{get_auth_header, global_checks}, + AppState, api::v1::auth::check_access_token, error::Error, objects::Me, utils::global_checks, }; -#[delete("/friends/{uuid}")] pub async fn delete( - req: HttpRequest, - path: web::Path<(Uuid,)>, - data: web::Data, -) -> Result { - let headers = req.headers(); + State(app_state): State>, + Path(friend_uuid): Path, + TypedHeader(auth): TypedHeader>, +) -> Result { + let mut conn = app_state.pool.get().await?; - let auth_header = get_auth_header(headers)?; + let uuid = check_access_token(auth.token(), &mut conn).await?; - let mut conn = data.pool.get().await?; - - let uuid = check_access_token(auth_header, &mut conn).await?; - - global_checks(&data, uuid).await?; + global_checks(&app_state, uuid).await?; let me = Me::get(&mut conn, uuid).await?; - me.remove_friend(&mut conn, path.0).await?; + me.remove_friend(&mut conn, friend_uuid).await?; - Ok(HttpResponse::Ok().finish()) + Ok(StatusCode::OK) } diff --git a/src/api/v1/me/guilds.rs b/src/api/v1/me/guilds.rs index 71cfca4..adfe845 100644 --- a/src/api/v1/me/guilds.rs +++ b/src/api/v1/me/guilds.rs @@ -1,13 +1,15 @@ //! `/api/v1/me/guilds` Contains endpoint related to guild memberships -use actix_web::{HttpRequest, HttpResponse, get, web}; +use std::sync::Arc; + +use axum::{Json, extract::State, http::StatusCode, response::IntoResponse}; +use axum_extra::{ + TypedHeader, + headers::{Authorization, authorization::Bearer}, +}; use crate::{ - Data, - api::v1::auth::check_access_token, - error::Error, - objects::Me, - utils::{get_auth_header, global_checks}, + AppState, api::v1::auth::check_access_token, error::Error, objects::Me, utils::global_checks, }; /// `GET /api/v1/me/guilds` Returns all guild memberships in a list @@ -55,21 +57,19 @@ use crate::{ /// ]); /// ``` /// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps -#[get("/guilds")] -pub async fn get(req: HttpRequest, data: web::Data) -> Result { - let headers = req.headers(); +pub async fn get( + State(app_state): State>, + TypedHeader(auth): TypedHeader>, +) -> Result { + let mut conn = app_state.pool.get().await?; - let auth_header = get_auth_header(headers)?; + let uuid = check_access_token(auth.token(), &mut conn).await?; - let mut conn = data.pool.get().await?; - - let uuid = check_access_token(auth_header, &mut conn).await?; - - global_checks(&data, uuid).await?; + global_checks(&app_state, uuid).await?; let me = Me::get(&mut conn, uuid).await?; let memberships = me.fetch_memberships(&mut conn).await?; - Ok(HttpResponse::Ok().json(memberships)) + Ok((StatusCode::OK, Json(memberships))) } diff --git a/src/api/v1/me/mod.rs b/src/api/v1/me/mod.rs index f667ca4..e9680bc 100644 --- a/src/api/v1/me/mod.rs +++ b/src/api/v1/me/mod.rs @@ -1,108 +1,120 @@ -use actix_multipart::form::{MultipartForm, json::Json as MpJson, tempfile::TempFile}; -use actix_web::{HttpRequest, HttpResponse, Scope, get, patch, web}; +use std::sync::Arc; + +use axum::{ + Json, Router, + extract::{DefaultBodyLimit, Multipart, State}, + http::StatusCode, + response::IntoResponse, + routing::{delete, get, patch, post}, +}; +use axum_extra::{ + TypedHeader, + headers::{Authorization, authorization::Bearer}, +}; +use bytes::Bytes; use serde::Deserialize; use crate::{ - Data, - api::v1::auth::check_access_token, - error::Error, - objects::Me, - utils::{get_auth_header, global_checks}, + AppState, api::v1::auth::check_access_token, error::Error, objects::Me, utils::global_checks, }; mod friends; mod guilds; -pub fn web() -> Scope { - web::scope("/me") - .service(get) - .service(update) - .service(guilds::get) - .service(friends::get) - .service(friends::post) - .service(friends::uuid::delete) +pub fn router() -> Router> { + Router::new() + .route("/", get(get_me)) + .route( + "/", + patch(update).layer(DefaultBodyLimit::max( + 100 * 1024 * 1024, /* limit is in bytes */ + )), + ) + .route("/guilds", get(guilds::get)) + .route("/friends", get(friends::get)) + .route("/friends", post(friends::post)) + .route("/friends/{uuid}", delete(friends::uuid::delete)) } -#[get("")] -pub async fn get(req: HttpRequest, data: web::Data) -> Result { - let headers = req.headers(); +pub async fn get_me( + State(app_state): State>, + TypedHeader(auth): TypedHeader>, +) -> Result { + let mut conn = app_state.pool.get().await?; - let auth_header = get_auth_header(headers)?; - - let mut conn = data.pool.get().await?; - - let uuid = check_access_token(auth_header, &mut conn).await?; + let uuid = check_access_token(auth.token(), &mut conn).await?; let me = Me::get(&mut conn, uuid).await?; - Ok(HttpResponse::Ok().json(me)) + Ok((StatusCode::OK, Json(me))) } -#[derive(Debug, Deserialize, Clone)] +#[derive(Default, Debug, Deserialize, Clone)] struct NewInfo { username: Option, display_name: Option, - //password: Option, will probably be handled through a reset password link email: Option, pronouns: Option, about: Option, } -#[derive(Debug, MultipartForm)] -struct UploadForm { - #[multipart(limit = "100MB")] - avatar: Option, - json: MpJson, -} - -#[patch("")] pub async fn update( - req: HttpRequest, - MultipartForm(form): MultipartForm, - data: web::Data, -) -> Result { - let headers = req.headers(); + State(app_state): State>, + TypedHeader(auth): TypedHeader>, + mut multipart: Multipart, +) -> Result { + let mut conn = app_state.pool.get().await?; - let auth_header = get_auth_header(headers)?; + let uuid = check_access_token(auth.token(), &mut conn).await?; - let mut conn = data.pool.get().await?; + let mut json_raw: Option = None; + let mut avatar: Option = None; - let uuid = check_access_token(auth_header, &mut conn).await?; + while let Some(field) = multipart.next_field().await.unwrap() { + let name = field + .name() + .ok_or(Error::BadRequest("Field has no name".to_string()))?; - if form.avatar.is_some() || form.json.username.is_some() || form.json.display_name.is_some() { - global_checks(&data, uuid).await?; + if name == "avatar" { + avatar = Some(field.bytes().await?); + } else if name == "json" { + json_raw = Some(serde_json::from_str(&field.text().await?)?) + } + } + + let json = json_raw.unwrap_or_default(); + + if avatar.is_some() || json.username.is_some() || json.display_name.is_some() { + global_checks(&app_state, uuid).await?; } let mut me = Me::get(&mut conn, uuid).await?; - if let Some(avatar) = form.avatar { - let bytes = tokio::fs::read(avatar.file).await?; - - let byte_slice: &[u8] = &bytes; - - me.set_avatar(&data, data.config.bunny.cdn_url.clone(), byte_slice.into()) + if let Some(avatar) = avatar { + me.set_avatar(&app_state, app_state.config.bunny.cdn_url.clone(), avatar) .await?; } - if let Some(username) = &form.json.username { - me.set_username(&data, username.clone()).await?; + if let Some(username) = &json.username { + me.set_username(&app_state, username.clone()).await?; } - if let Some(display_name) = &form.json.display_name { - me.set_display_name(&data, display_name.clone()).await?; + if let Some(display_name) = &json.display_name { + me.set_display_name(&app_state, display_name.clone()) + .await?; } - if let Some(email) = &form.json.email { - me.set_email(&data, email.clone()).await?; + if let Some(email) = &json.email { + me.set_email(&app_state, email.clone()).await?; } - if let Some(pronouns) = &form.json.pronouns { - me.set_pronouns(&data, pronouns.clone()).await?; + if let Some(pronouns) = &json.pronouns { + me.set_pronouns(&app_state, pronouns.clone()).await?; } - if let Some(about) = &form.json.about { - me.set_about(&data, about.clone()).await?; + if let Some(about) = &json.about { + me.set_about(&app_state, about.clone()).await?; } - Ok(HttpResponse::Ok().finish()) + Ok(StatusCode::OK) } diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs index 6c2df0b..4e8654b 100644 --- a/src/api/v1/mod.rs +++ b/src/api/v1/mod.rs @@ -1,6 +1,10 @@ //! `/api/v1` Contains version 1 of the api -use actix_web::{Scope, web}; +use std::sync::Arc; + +use axum::{routing::get, Router}; + +use crate::AppState; mod auth; mod channels; @@ -10,13 +14,13 @@ mod me; mod stats; mod users; -pub fn web() -> Scope { - web::scope("/v1") - .service(stats::res) - .service(auth::web()) - .service(users::web()) - .service(channels::web()) - .service(guilds::web()) - .service(invites::web()) - .service(me::web()) +pub fn router() -> Router> { + Router::new() + .route("/stats", get(stats::res)) + .nest("/auth", auth::router()) + .nest("/users", users::router()) + .nest("/channels", channels::router()) + .nest("/guilds", guilds::router()) + .nest("/invites", invites::router()) + .nest("/me", me::router()) } diff --git a/src/api/v1/stats.rs b/src/api/v1/stats.rs index 760ec71..17c5df6 100644 --- a/src/api/v1/stats.rs +++ b/src/api/v1/stats.rs @@ -1,13 +1,17 @@ //! `/api/v1/stats` Returns stats about the server +use std::sync::Arc; use std::time::SystemTime; -use actix_web::{HttpResponse, get, web}; +use axum::Json; +use axum::extract::State; +use axum::http::StatusCode; +use axum::response::IntoResponse; use diesel::QueryDsl; use diesel_async::RunQueryDsl; use serde::Serialize; -use crate::Data; +use crate::AppState; use crate::error::Error; use crate::schema::users::dsl::{users, uuid}; @@ -39,27 +43,26 @@ struct Response { /// "build_number": "39d01bb" /// }); /// ``` -#[get("/stats")] -pub async fn res(data: web::Data) -> Result { +pub async fn res(State(app_state): State>) -> Result { let accounts: i64 = users .select(uuid) .count() - .get_result(&mut data.pool.get().await?) + .get_result(&mut app_state.pool.get().await?) .await?; let response = Response { // TODO: Get number of accounts from db accounts, uptime: SystemTime::now() - .duration_since(data.start_time) + .duration_since(app_state.start_time) .expect("Seriously why dont you have time??") .as_secs(), version: String::from(VERSION.unwrap_or("UNKNOWN")), - registration_enabled: data.config.instance.registration, - email_verification_required: data.config.instance.require_email_verification, + registration_enabled: app_state.config.instance.registration, + email_verification_required: app_state.config.instance.require_email_verification, // TODO: Get build number from git hash or remove this from the spec build_number: String::from(GIT_SHORT_HASH), }; - Ok(HttpResponse::Ok().json(response)) + Ok((StatusCode::OK, Json(response))) } diff --git a/src/api/v1/users/mod.rs b/src/api/v1/users/mod.rs index 334fd5f..f0d09c5 100644 --- a/src/api/v1/users/mod.rs +++ b/src/api/v1/users/mod.rs @@ -1,19 +1,33 @@ //! `/api/v1/users` Contains endpoints related to all users -use actix_web::{HttpRequest, HttpResponse, Scope, get, web}; +use std::sync::Arc; + +use axum::{ + Json, Router, + extract::{Query, State}, + http::StatusCode, + response::IntoResponse, + routing::get, +}; +use axum_extra::{ + TypedHeader, + headers::{Authorization, authorization::Bearer}, +}; use crate::{ - Data, + AppState, api::v1::auth::check_access_token, error::Error, objects::{StartAmountQuery, User}, - utils::{get_auth_header, global_checks}, + utils::global_checks, }; mod uuid; -pub fn web() -> Scope { - web::scope("/users").service(get).service(uuid::get) +pub fn router() -> Router> { + Router::new() + .route("/", get(users)) + .route("/{uuid}", get(uuid::get)) } /// `GET /api/v1/users` Returns all users on this instance @@ -46,31 +60,26 @@ pub fn web() -> Scope { /// ]); /// ``` /// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps -#[get("")] -pub async fn get( - req: HttpRequest, - request_query: web::Query, - data: web::Data, -) -> Result { - let headers = req.headers(); - - let auth_header = get_auth_header(headers)?; - +pub async fn users( + State(app_state): State>, + Query(request_query): Query, + TypedHeader(auth): TypedHeader>, +) -> Result { let start = request_query.start.unwrap_or(0); let amount = request_query.amount.unwrap_or(10); if amount > 100 { - return Ok(HttpResponse::BadRequest().finish()); + return Ok(StatusCode::BAD_REQUEST.into_response()); } - let mut conn = data.pool.get().await?; + let mut conn = app_state.pool.get().await?; - let uuid = check_access_token(auth_header, &mut conn).await?; + let uuid = check_access_token(auth.token(), &mut conn).await?; - global_checks(&data, uuid).await?; + global_checks(&app_state, uuid).await?; let users = User::fetch_amount(&mut conn, start, amount).await?; - Ok(HttpResponse::Ok().json(users)) + Ok((StatusCode::OK, Json(users)).into_response()) } diff --git a/src/api/v1/users/uuid.rs b/src/api/v1/users/uuid.rs index 5d36b75..1b7d43b 100644 --- a/src/api/v1/users/uuid.rs +++ b/src/api/v1/users/uuid.rs @@ -1,14 +1,25 @@ //! `/api/v1/users/{uuid}` Specific user endpoints -use actix_web::{HttpRequest, HttpResponse, get, web}; +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use axum_extra::{ + TypedHeader, + headers::{Authorization, authorization::Bearer}, +}; use uuid::Uuid; use crate::{ - Data, + AppState, api::v1::auth::check_access_token, error::Error, objects::{Me, User}, - utils::{get_auth_header, global_checks}, + utils::global_checks, }; /// `GET /api/v1/users/{uuid}` Returns user with the given UUID @@ -27,27 +38,20 @@ use crate::{ /// }); /// ``` /// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps -#[get("/{uuid}")] pub async fn get( - req: HttpRequest, - path: web::Path<(Uuid,)>, - data: web::Data, -) -> Result { - let headers = req.headers(); + State(app_state): State>, + Path(user_uuid): Path, + TypedHeader(auth): TypedHeader>, +) -> Result { + let mut conn = app_state.pool.get().await?; - let user_uuid = path.into_inner().0; + let uuid = check_access_token(auth.token(), &mut conn).await?; - let auth_header = get_auth_header(headers)?; - - let mut conn = data.pool.get().await?; - - let uuid = check_access_token(auth_header, &mut conn).await?; - - global_checks(&data, uuid).await?; + global_checks(&app_state, uuid).await?; let me = Me::get(&mut conn, uuid).await?; - let user = User::fetch_one_with_friendship(&data, &me, user_uuid).await?; + let user = User::fetch_one_with_friendship(&app_state, &me, user_uuid).await?; - Ok(HttpResponse::Ok().json(user)) + Ok((StatusCode::OK, Json(user))) } diff --git a/src/api/versions.rs b/src/api/versions.rs index 0c3e106..3c9576b 100644 --- a/src/api/versions.rs +++ b/src/api/versions.rs @@ -1,5 +1,5 @@ //! `/api/v1/versions` Returns info about api versions -use actix_web::{HttpResponse, Responder, get}; +use axum::{Json, http::StatusCode, response::IntoResponse}; use serde::Serialize; #[derive(Serialize)] @@ -24,13 +24,12 @@ struct UnstableFeatures; /// ] /// }); /// ``` -#[get("/versions")] -pub async fn get() -> impl Responder { +pub async fn versions() -> impl IntoResponse { let response = Response { unstable_features: UnstableFeatures, // TODO: Find a way to dynamically update this possibly? versions: vec![String::from("1")], }; - HttpResponse::Ok().json(response) + (StatusCode::OK, Json(response)) } diff --git a/src/error.rs b/src/error.rs index 35b533d..1b8f27c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,12 +1,16 @@ use std::{io, time::SystemTimeError}; -use actix_web::{ - HttpResponse, - error::{PayloadError, ResponseError}, +use axum::{ + Json, + extract::{ + multipart::MultipartError, + rejection::{JsonRejection, QueryRejection}, + }, http::{ StatusCode, - header::{ContentType, ToStrError}, + header::{InvalidHeaderValue, ToStrError}, }, + response::IntoResponse, }; use bunny_api_tokio::error::Error as BunnyError; use deadpool::managed::{BuildError, PoolError}; @@ -54,9 +58,13 @@ pub enum Error { #[error(transparent)] UrlParseError(#[from] url::ParseError), #[error(transparent)] - PayloadError(#[from] PayloadError), + JsonRejection(#[from] JsonRejection), #[error(transparent)] - WsClosed(#[from] actix_ws::Closed), + QueryRejection(#[from] QueryRejection), + #[error(transparent)] + MultipartError(#[from] MultipartError), + #[error(transparent)] + InvalidHeaderValue(#[from] InvalidHeaderValue), #[error(transparent)] EmailError(#[from] EmailError), #[error(transparent)] @@ -77,26 +85,40 @@ pub enum Error { InternalServerError(String), } -impl ResponseError for Error { - fn error_response(&self) -> HttpResponse { +impl IntoResponse for Error { + fn into_response(self) -> axum::response::Response { + let error = match self { + Error::SqlError(DieselError::NotFound) => { + (StatusCode::NOT_FOUND, Json(WebError::new(self.to_string()))) + } + Error::BunnyError(BunnyError::NotFound(_)) => { + (StatusCode::NOT_FOUND, Json(WebError::new(self.to_string()))) + } + Error::BadRequest(_) => ( + StatusCode::BAD_REQUEST, + Json(WebError::new(self.to_string())), + ), + Error::Unauthorized(_) => ( + StatusCode::UNAUTHORIZED, + Json(WebError::new(self.to_string())), + ), + Error::Forbidden(_) => (StatusCode::FORBIDDEN, Json(WebError::new(self.to_string()))), + Error::TooManyRequests(_) => ( + StatusCode::TOO_MANY_REQUESTS, + Json(WebError::new(self.to_string())), + ), + _ => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(WebError::new(self.to_string())), + ), + }; + + let (code, _) = error; + debug!("{self:?}"); - error!("{}: {}", self.status_code(), self); + error!("{code}: {self}"); - HttpResponse::build(self.status_code()) - .insert_header(ContentType::json()) - .json(WebError::new(self.to_string())) - } - - fn status_code(&self) -> StatusCode { - match *self { - Error::SqlError(DieselError::NotFound) => StatusCode::NOT_FOUND, - Error::BunnyError(BunnyError::NotFound(_)) => StatusCode::NOT_FOUND, - Error::BadRequest(_) => StatusCode::BAD_REQUEST, - Error::Unauthorized(_) => StatusCode::UNAUTHORIZED, - Error::Forbidden(_) => StatusCode::FORBIDDEN, - Error::TooManyRequests(_) => StatusCode::TOO_MANY_REQUESTS, - _ => StatusCode::INTERNAL_SERVER_ERROR, - } + error.into_response() } } diff --git a/src/main.rs b/src/main.rs index 248289a..6bb2be3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,13 @@ -use actix_cors::Cors; -use actix_web::{App, HttpServer, web}; use argon2::Argon2; +use axum::Router; use clap::Parser; use diesel_async::pooled_connection::AsyncDieselConnectionManager; use diesel_async::pooled_connection::deadpool::Pool; use error::Error; use objects::MailClient; -use simple_logger::SimpleLogger; -use std::time::SystemTime; +use socketioxide::SocketIo; +use std::{sync::Arc, time::SystemTime}; +use tower_http::cors::{Any, CorsLayer}; mod config; use config::{Config, ConfigBuilder}; use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations}; @@ -22,6 +22,7 @@ pub mod error; pub mod objects; pub mod schema; pub mod utils; +mod socket; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] @@ -31,7 +32,7 @@ struct Args { } #[derive(Clone)] -pub struct Data { +pub struct AppState { pub pool: deadpool::managed::Pool< AsyncDieselConnectionManager, Conn, @@ -46,12 +47,14 @@ pub struct Data { #[tokio::main] async fn main() -> Result<(), Error> { - SimpleLogger::new() - .with_level(log::LevelFilter::Info) - .with_colors(true) - .env() - .init() - .unwrap(); + tracing_subscriber::fmt::init(); + + //SimpleLogger::new() + // .with_level(log::LevelFilter::Info) + // .with_colors(true) + // .env() + // .init() + // .unwrap(); let args = Args::parse(); let config = ConfigBuilder::load(args.config).await?.build(); @@ -112,7 +115,7 @@ async fn main() -> Result<(), Error> { ) */ - let data = Data { + let app_state = Arc::new(AppState { pool, cache_pool, config, @@ -121,42 +124,31 @@ async fn main() -> Result<(), Error> { start_time: SystemTime::now(), bunny_storage, mail_client, - }; + }); - HttpServer::new(move || { - // Set CORS headers - let cors = Cors::default() - /* - Set Allowed-Control-Allow-Origin header to whatever - the request's Origin header is. Must be done like this - rather than setting it to "*" due to CORS not allowing - sending of credentials (cookies) with wildcard origin. - */ - .allowed_origin_fn(|_origin, _req_head| true) - /* - Allows any request method in CORS preflight requests. - This will be restricted to only ones actually in use later. - */ - .allow_any_method() - /* - Allows any header(s) in request in CORS preflight requests. - This wll be restricted to only ones actually in use later. - */ - .allow_any_header() - /* - Allows browser to include cookies in requests. - This is needed for receiving the secure HttpOnly refresh_token cookie. - */ - .supports_credentials(); + let cors = CorsLayer::new() + // Allow any origin (equivalent to allowed_origin_fn returning true) + .allow_origin(Any) + // Allow any method + .allow_methods(Any) + // Allow any headers + .allow_headers(Any); - App::new() - .app_data(web::Data::new(data.clone())) - .wrap(cors) - .service(api::web(data.config.web.backend_url.path())) - }) - .bind((web.ip, web.port))? - .run() - .await?; + let (socket_io, io) = SocketIo::builder().with_state(app_state.clone()).build_layer(); + + io.ns("/", socket::on_connect); + + // build our application with a route + let app = Router::new() + // `GET /` goes to `root` + .nest(web.backend_url.path(), api::router()) + .with_state(app_state) + .layer(cors) + .layer(socket_io); + + // run our app with hyper, listening globally on port 3000 + let listener = tokio::net::TcpListener::bind(web.ip + ":" + &web.port.to_string()).await?; + axum::serve(listener, app).await?; Ok(()) } diff --git a/src/objects/channel.rs b/src/objects/channel.rs index 1192c69..3b34ac6 100644 --- a/src/objects/channel.rs +++ b/src/objects/channel.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ - Conn, Data, + AppState, Conn, error::Error, schema::{channel_permissions, channels, messages}, utils::{CHANNEL_REGEX, order_by_is_above}, @@ -105,12 +105,12 @@ impl Channel { futures::future::try_join_all(channel_futures).await } - pub async fn fetch_one(data: &Data, channel_uuid: Uuid) -> Result { - if let Ok(cache_hit) = data.get_cache_key(channel_uuid.to_string()).await { + pub async fn fetch_one(app_state: &AppState, channel_uuid: Uuid) -> Result { + if let Ok(cache_hit) = app_state.get_cache_key(channel_uuid.to_string()).await { return Ok(serde_json::from_str(&cache_hit)?); } - let mut conn = data.pool.get().await?; + let mut conn = app_state.pool.get().await?; use channels::dsl; let channel_builder: ChannelBuilder = dsl::channels @@ -121,14 +121,15 @@ impl Channel { let channel = channel_builder.build(&mut conn).await?; - data.set_cache_key(channel_uuid.to_string(), channel.clone(), 60) + app_state + .set_cache_key(channel_uuid.to_string(), channel.clone(), 60) .await?; Ok(channel) } pub async fn new( - data: actix_web::web::Data, + app_state: &AppState, guild_uuid: Uuid, name: String, description: Option, @@ -137,11 +138,11 @@ impl Channel { return Err(Error::BadRequest("Channel name is invalid".to_string())); } - let mut conn = data.pool.get().await?; + let mut conn = app_state.pool.get().await?; let channel_uuid = Uuid::now_v7(); - let channels = Self::fetch_all(&data.pool, guild_uuid).await?; + let channels = Self::fetch_all(&app_state.pool, guild_uuid).await?; let channels_ordered = order_by_is_above(channels).await?; @@ -179,22 +180,25 @@ impl Channel { permissions: vec![], }; - data.set_cache_key(channel_uuid.to_string(), channel.clone(), 1800) + app_state + .set_cache_key(channel_uuid.to_string(), channel.clone(), 1800) .await?; - if data + if app_state .get_cache_key(format!("{guild_uuid}_channels")) .await .is_ok() { - data.del_cache_key(format!("{guild_uuid}_channels")).await?; + app_state + .del_cache_key(format!("{guild_uuid}_channels")) + .await?; } Ok(channel) } - pub async fn delete(self, data: &Data) -> Result<(), Error> { - let mut conn = data.pool.get().await?; + pub async fn delete(self, app_state: &AppState) -> Result<(), Error> { + let mut conn = app_state.pool.get().await?; use channels::dsl; match update(channels::table) @@ -224,16 +228,17 @@ impl Channel { Err(e) => Err(e), }?; - if data.get_cache_key(self.uuid.to_string()).await.is_ok() { - data.del_cache_key(self.uuid.to_string()).await?; + if app_state.get_cache_key(self.uuid.to_string()).await.is_ok() { + app_state.del_cache_key(self.uuid.to_string()).await?; } - if data + if app_state .get_cache_key(format!("{}_channels", self.guild_uuid)) .await .is_ok() { - data.del_cache_key(format!("{}_channels", self.guild_uuid)) + app_state + .del_cache_key(format!("{}_channels", self.guild_uuid)) .await?; } @@ -242,11 +247,11 @@ impl Channel { pub async fn fetch_messages( &self, - data: &Data, + app_state: &AppState, amount: i64, offset: i64, ) -> Result, Error> { - let mut conn = data.pool.get().await?; + let mut conn = app_state.pool.get().await?; use messages::dsl; let messages: Vec = load_or_empty( @@ -260,14 +265,14 @@ impl Channel { .await, )?; - let message_futures = messages.iter().map(async move |b| b.build(data).await); + let message_futures = messages.iter().map(async move |b| b.build(app_state).await); futures::future::try_join_all(message_futures).await } pub async fn new_message( &self, - data: &Data, + app_state: &AppState, user_uuid: Uuid, message: String, reply_to: Option, @@ -282,22 +287,22 @@ impl Channel { reply_to, }; - let mut conn = data.pool.get().await?; + let mut conn = app_state.pool.get().await?; insert_into(messages::table) .values(message.clone()) .execute(&mut conn) .await?; - message.build(data).await + message.build(app_state).await } - pub async fn set_name(&mut self, data: &Data, new_name: String) -> Result<(), Error> { + pub async fn set_name(&mut self, app_state: &AppState, new_name: String) -> Result<(), Error> { if !CHANNEL_REGEX.is_match(&new_name) { return Err(Error::BadRequest("Channel name is invalid".to_string())); } - let mut conn = data.pool.get().await?; + let mut conn = app_state.pool.get().await?; use channels::dsl; update(channels::table) @@ -313,10 +318,10 @@ impl Channel { pub async fn set_description( &mut self, - data: &Data, + app_state: &AppState, new_description: String, ) -> Result<(), Error> { - let mut conn = data.pool.get().await?; + let mut conn = app_state.pool.get().await?; use channels::dsl; update(channels::table) @@ -330,8 +335,12 @@ impl Channel { Ok(()) } - pub async fn move_channel(&mut self, data: &Data, new_is_above: Uuid) -> Result<(), Error> { - let mut conn = data.pool.get().await?; + pub async fn move_channel( + &mut self, + app_state: &AppState, + new_is_above: Uuid, + ) -> Result<(), Error> { + let mut conn = app_state.pool.get().await?; use channels::dsl; let old_above_uuid: Option = match dsl::channels diff --git a/src/objects/email_token.rs b/src/objects/email_token.rs index bfd1ef5..64d2fdb 100644 --- a/src/objects/email_token.rs +++ b/src/objects/email_token.rs @@ -3,7 +3,7 @@ use lettre::message::MultiPart; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::{Data, error::Error, utils::generate_token}; +use crate::{AppState, error::Error, utils::generate_token}; use super::Me; @@ -15,9 +15,9 @@ pub struct EmailToken { } impl EmailToken { - pub async fn get(data: &Data, user_uuid: Uuid) -> Result { + pub async fn get(app_state: &AppState, user_uuid: Uuid) -> Result { let email_token = serde_json::from_str( - &data + &app_state .get_cache_key(format!("{user_uuid}_email_verify")) .await?, )?; @@ -26,7 +26,7 @@ impl EmailToken { } #[allow(clippy::new_ret_no_self)] - pub async fn new(data: &Data, me: Me) -> Result<(), Error> { + pub async fn new(app_state: &AppState, me: Me) -> Result<(), Error> { let token = generate_token::<32>()?; let email_token = EmailToken { @@ -36,30 +36,32 @@ impl EmailToken { created_at: Utc::now(), }; - data.set_cache_key(format!("{}_email_verify", me.uuid), email_token, 86400) + app_state + .set_cache_key(format!("{}_email_verify", me.uuid), email_token, 86400) .await?; - let mut verify_endpoint = data.config.web.frontend_url.join("verify-email")?; + let mut verify_endpoint = app_state.config.web.frontend_url.join("verify-email")?; verify_endpoint.set_query(Some(&format!("token={token}"))); - let email = data + let email = app_state .mail_client .message_builder() .to(me.email.parse()?) - .subject(format!("{} E-mail Verification", data.config.instance.name)) + .subject(format!("{} E-mail Verification", app_state.config.instance.name)) .multipart(MultiPart::alternative_plain_html( - format!("Verify your {} account\n\nHello, {}!\nThanks for creating a new account on Gorb.\nThe final step to create your account is to verify your email address by visiting the page, within 24 hours.\n\n{}\n\nIf you didn't ask to verify this address, you can safely ignore this email\n\nThanks, The gorb team.", data.config.instance.name, me.username, verify_endpoint), - format!(r#"

Verify your {} Account

Hello, {}!

Thanks for creating a new account on Gorb.

The final step to create your account is to verify your email address by clicking the button below, within 24 hours.

VERIFY ACCOUNT

If you didn't ask to verify this address, you can safely ignore this email.

"#, data.config.instance.name, me.username, verify_endpoint) + format!("Verify your {} account\n\nHello, {}!\nThanks for creating a new account on Gorb.\nThe final step to create your account is to verify your email address by visiting the page, within 24 hours.\n\n{}\n\nIf you didn't ask to verify this address, you can safely ignore this email\n\nThanks, The gorb team.", app_state.config.instance.name, me.username, verify_endpoint), + format!(r#"

Verify your {} Account

Hello, {}!

Thanks for creating a new account on Gorb.

The final step to create your account is to verify your email address by clicking the button below, within 24 hours.

VERIFY ACCOUNT

If you didn't ask to verify this address, you can safely ignore this email.

"#, app_state.config.instance.name, me.username, verify_endpoint) ))?; - data.mail_client.send_mail(email).await?; + app_state.mail_client.send_mail(email).await?; Ok(()) } - pub async fn delete(&self, data: &Data) -> Result<(), Error> { - data.del_cache_key(format!("{}_email_verify", self.user_uuid)) + pub async fn delete(&self, app_state: &AppState) -> Result<(), Error> { + app_state + .del_cache_key(format!("{}_email_verify", self.user_uuid)) .await?; Ok(()) diff --git a/src/objects/guild.rs b/src/objects/guild.rs index aa01f54..e27e129 100644 --- a/src/objects/guild.rs +++ b/src/objects/guild.rs @@ -1,4 +1,4 @@ -use actix_web::web::BytesMut; +use axum::body::Bytes; use diesel::{ ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, insert_into, update, @@ -191,7 +191,7 @@ impl Guild { bunny_storage: &bunny_api_tokio::EdgeStorageClient, conn: &mut Conn, cdn_url: Url, - icon: BytesMut, + icon: Bytes, ) -> Result<(), Error> { let icon_clone = icon.clone(); let image_type = task::spawn_blocking(move || image_check(icon_clone)).await??; @@ -204,7 +204,7 @@ impl Guild { let path = format!("icons/{}/{}.{}", self.uuid, Uuid::now_v7(), image_type); - bunny_storage.upload(path.clone(), icon.into()).await?; + bunny_storage.upload(path.clone(), icon).await?; let icon_url = cdn_url.join(&path)?; diff --git a/src/objects/me.rs b/src/objects/me.rs index 37951ab..3b51da4 100644 --- a/src/objects/me.rs +++ b/src/objects/me.rs @@ -1,4 +1,4 @@ -use actix_web::web::BytesMut; +use axum::body::Bytes; use diesel::{ ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper, delete, insert_into, update, @@ -10,7 +10,7 @@ use url::Url; use uuid::Uuid; use crate::{ - Conn, Data, + AppState, Conn, error::Error, objects::{Friend, FriendRequest, User}, schema::{friend_requests, friends, guild_members, guilds, users}, @@ -75,28 +75,26 @@ impl Me { pub async fn set_avatar( &mut self, - data: &Data, + app_state: &AppState, cdn_url: Url, - avatar: BytesMut, + avatar: Bytes, ) -> Result<(), Error> { let avatar_clone = avatar.clone(); let image_type = task::spawn_blocking(move || image_check(avatar_clone)).await??; - let mut conn = data.pool.get().await?; + let mut conn = app_state.pool.get().await?; if let Some(avatar) = &self.avatar { let avatar_url: Url = avatar.parse()?; let relative_url = avatar_url.path().trim_start_matches('/'); - data.bunny_storage.delete(relative_url).await?; + app_state.bunny_storage.delete(relative_url).await?; } let path = format!("avatar/{}/{}.{}", self.uuid, Uuid::now_v7(), image_type); - data.bunny_storage - .upload(path.clone(), avatar.into()) - .await?; + app_state.bunny_storage.upload(path.clone(), avatar).await?; let avatar_url = cdn_url.join(&path)?; @@ -107,8 +105,8 @@ impl Me { .execute(&mut conn) .await?; - if data.get_cache_key(self.uuid.to_string()).await.is_ok() { - data.del_cache_key(self.uuid.to_string()).await? + if app_state.get_cache_key(self.uuid.to_string()).await.is_ok() { + app_state.del_cache_key(self.uuid.to_string()).await? } self.avatar = Some(avatar_url.to_string()); @@ -127,7 +125,11 @@ impl Me { Ok(()) } - pub async fn set_username(&mut self, data: &Data, new_username: String) -> Result<(), Error> { + pub async fn set_username( + &mut self, + app_state: &AppState, + new_username: String, + ) -> Result<(), Error> { if !USERNAME_REGEX.is_match(&new_username) || new_username.len() < 3 || new_username.len() > 32 @@ -135,7 +137,7 @@ impl Me { return Err(Error::BadRequest("Invalid username".to_string())); } - let mut conn = data.pool.get().await?; + let mut conn = app_state.pool.get().await?; use users::dsl; update(users::table) @@ -144,8 +146,8 @@ impl Me { .execute(&mut conn) .await?; - if data.get_cache_key(self.uuid.to_string()).await.is_ok() { - data.del_cache_key(self.uuid.to_string()).await? + if app_state.get_cache_key(self.uuid.to_string()).await.is_ok() { + app_state.del_cache_key(self.uuid.to_string()).await? } self.username = new_username; @@ -155,10 +157,10 @@ impl Me { pub async fn set_display_name( &mut self, - data: &Data, + app_state: &AppState, new_display_name: String, ) -> Result<(), Error> { - let mut conn = data.pool.get().await?; + let mut conn = app_state.pool.get().await?; let new_display_name_option = if new_display_name.is_empty() { None @@ -173,8 +175,8 @@ impl Me { .execute(&mut conn) .await?; - if data.get_cache_key(self.uuid.to_string()).await.is_ok() { - data.del_cache_key(self.uuid.to_string()).await? + if app_state.get_cache_key(self.uuid.to_string()).await.is_ok() { + app_state.del_cache_key(self.uuid.to_string()).await? } self.display_name = new_display_name_option; @@ -182,12 +184,16 @@ impl Me { Ok(()) } - pub async fn set_email(&mut self, data: &Data, new_email: String) -> Result<(), Error> { + pub async fn set_email( + &mut self, + app_state: &AppState, + new_email: String, + ) -> Result<(), Error> { if !EMAIL_REGEX.is_match(&new_email) { return Err(Error::BadRequest("Invalid username".to_string())); } - let mut conn = data.pool.get().await?; + let mut conn = app_state.pool.get().await?; use users::dsl; update(users::table) @@ -199,8 +205,8 @@ impl Me { .execute(&mut conn) .await?; - if data.get_cache_key(self.uuid.to_string()).await.is_ok() { - data.del_cache_key(self.uuid.to_string()).await? + if app_state.get_cache_key(self.uuid.to_string()).await.is_ok() { + app_state.del_cache_key(self.uuid.to_string()).await? } self.email = new_email; @@ -208,8 +214,12 @@ impl Me { Ok(()) } - pub async fn set_pronouns(&mut self, data: &Data, new_pronouns: String) -> Result<(), Error> { - let mut conn = data.pool.get().await?; + pub async fn set_pronouns( + &mut self, + app_state: &AppState, + new_pronouns: String, + ) -> Result<(), Error> { + let mut conn = app_state.pool.get().await?; use users::dsl; update(users::table) @@ -218,15 +228,19 @@ impl Me { .execute(&mut conn) .await?; - if data.get_cache_key(self.uuid.to_string()).await.is_ok() { - data.del_cache_key(self.uuid.to_string()).await? + if app_state.get_cache_key(self.uuid.to_string()).await.is_ok() { + app_state.del_cache_key(self.uuid.to_string()).await? } Ok(()) } - pub async fn set_about(&mut self, data: &Data, new_about: String) -> Result<(), Error> { - let mut conn = data.pool.get().await?; + pub async fn set_about( + &mut self, + app_state: &AppState, + new_about: String, + ) -> Result<(), Error> { + let mut conn = app_state.pool.get().await?; use users::dsl; update(users::table) @@ -235,8 +249,8 @@ impl Me { .execute(&mut conn) .await?; - if data.get_cache_key(self.uuid.to_string()).await.is_ok() { - data.del_cache_key(self.uuid.to_string()).await? + if app_state.get_cache_key(self.uuid.to_string()).await.is_ok() { + app_state.del_cache_key(self.uuid.to_string()).await? } Ok(()) @@ -352,10 +366,10 @@ impl Me { Ok(()) } - pub async fn get_friends(&self, data: &Data) -> Result, Error> { + pub async fn get_friends(&self, app_state: &AppState) -> Result, Error> { use friends::dsl; - let mut conn = data.pool.get().await?; + let mut conn = app_state.pool.get().await?; let friends1 = load_or_empty( dsl::friends @@ -374,13 +388,13 @@ impl Me { )?; let friend_futures = friends1.iter().map(async move |friend| { - User::fetch_one_with_friendship(data, self, friend.uuid2).await + User::fetch_one_with_friendship(app_state, self, friend.uuid2).await }); let mut friends = futures::future::try_join_all(friend_futures).await?; let friend_futures = friends2.iter().map(async move |friend| { - User::fetch_one_with_friendship(data, self, friend.uuid1).await + User::fetch_one_with_friendship(app_state, self, friend.uuid1).await }); friends.append(&mut futures::future::try_join_all(friend_futures).await?); diff --git a/src/objects/member.rs b/src/objects/member.rs index c2a71d9..50b76b0 100644 --- a/src/objects/member.rs +++ b/src/objects/member.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ - Conn, Data, + AppState, Conn, error::Error, objects::{Me, Permissions, Role}, schema::guild_members, @@ -26,13 +26,13 @@ pub struct MemberBuilder { } impl MemberBuilder { - pub async fn build(&self, data: &Data, me: Option<&Me>) -> Result { + pub async fn build(&self, app_state: &AppState, me: Option<&Me>) -> Result { let user; if let Some(me) = me { - user = User::fetch_one_with_friendship(data, me, self.user_uuid).await?; + user = User::fetch_one_with_friendship(app_state, me, self.user_uuid).await?; } else { - user = User::fetch_one(data, self.user_uuid).await?; + user = User::fetch_one(app_state, self.user_uuid).await?; } Ok(Member { @@ -47,11 +47,11 @@ impl MemberBuilder { pub async fn check_permission( &self, - data: &Data, + app_state: &AppState, permission: Permissions, ) -> Result<(), Error> { if !self.is_owner { - let roles = Role::fetch_from_member(data, self.uuid).await?; + let roles = Role::fetch_from_member(app_state, self.uuid).await?; let allowed = roles.iter().any(|r| r.permissions & permission as i64 != 0); if !allowed { return Err(Error::Forbidden("Not allowed".to_string())); @@ -101,12 +101,12 @@ impl Member { } pub async fn fetch_one( - data: &Data, + app_state: &AppState, me: &Me, user_uuid: Uuid, guild_uuid: Uuid, ) -> Result { - let mut conn = data.pool.get().await?; + let mut conn = app_state.pool.get().await?; use guild_members::dsl; let member: MemberBuilder = dsl::guild_members @@ -116,11 +116,15 @@ impl Member { .get_result(&mut conn) .await?; - member.build(data, Some(me)).await + member.build(app_state, Some(me)).await } - pub async fn fetch_all(data: &Data, me: &Me, guild_uuid: Uuid) -> Result, Error> { - let mut conn = data.pool.get().await?; + pub async fn fetch_all( + app_state: &AppState, + me: &Me, + guild_uuid: Uuid, + ) -> Result, Error> { + let mut conn = app_state.pool.get().await?; use guild_members::dsl; let member_builders: Vec = load_or_empty( @@ -134,14 +138,18 @@ impl Member { let mut members = vec![]; for builder in member_builders { - members.push(builder.build(&data, Some(me)).await?); + members.push(builder.build(app_state, Some(me)).await?); } Ok(members) } - pub async fn new(data: &Data, user_uuid: Uuid, guild_uuid: Uuid) -> Result { - let mut conn = data.pool.get().await?; + pub async fn new( + app_state: &AppState, + user_uuid: Uuid, + guild_uuid: Uuid, + ) -> Result { + let mut conn = app_state.pool.get().await?; let member_uuid = Uuid::now_v7(); @@ -158,6 +166,6 @@ impl Member { .execute(&mut conn) .await?; - member.build(data, None).await + member.build(app_state, None).await } } diff --git a/src/objects/message.rs b/src/objects/message.rs index a887541..caff969 100644 --- a/src/objects/message.rs +++ b/src/objects/message.rs @@ -2,7 +2,7 @@ use diesel::{Insertable, Queryable, Selectable}; use serde::Serialize; use uuid::Uuid; -use crate::{Data, error::Error, schema::messages}; +use crate::{AppState, error::Error, schema::messages}; use super::User; @@ -18,8 +18,8 @@ pub struct MessageBuilder { } impl MessageBuilder { - pub async fn build(&self, data: &Data) -> Result { - let user = User::fetch_one(data, self.user_uuid).await?; + pub async fn build(&self, app_state: &AppState) -> Result { + let user = User::fetch_one(app_state, self.user_uuid).await?; Ok(Message { uuid: self.uuid, diff --git a/src/objects/mod.rs b/src/objects/mod.rs index 9974410..4af16d8 100644 --- a/src/objects/mod.rs +++ b/src/objects/mod.rs @@ -42,6 +42,37 @@ pub trait HasUuid { pub trait HasIsAbove { fn is_above(&self) -> Option<&Uuid>; } +/* +pub trait Cookies { + fn cookies(&self) -> CookieJar; + fn cookie>(&self, cookie: T) -> Option; +} + +impl Cookies for Request { + fn cookies(&self) -> CookieJar { + let cookies = self.headers() + .get(axum::http::header::COOKIE) + .and_then(|value| value.to_str().ok()) + .map(|s| Cookie::split_parse(s.to_string())) + .and_then(|c| c.collect::, cookie::ParseError>>().ok()) + .unwrap_or(vec![]); + + let mut cookie_jar = CookieJar::new(); + + for cookie in cookies { + cookie_jar.add(cookie) + } + + cookie_jar + } + + fn cookie>(&self, cookie: T) -> Option { + self.cookies() + .get(cookie.as_ref()) + .and_then(|c| Some(c.to_owned())) + } +} +*/ fn load_or_empty( query_result: Result, diesel::result::Error>, diff --git a/src/objects/password_reset_token.rs b/src/objects/password_reset_token.rs index 7f714ef..04ff43c 100644 --- a/src/objects/password_reset_token.rs +++ b/src/objects/password_reset_token.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ - Data, + AppState, error::Error, schema::users, utils::{PASSWORD_REGEX, generate_token, global_checks, user_uuid_from_identifier}, @@ -24,10 +24,11 @@ pub struct PasswordResetToken { } impl PasswordResetToken { - pub async fn get(data: &Data, token: String) -> Result { - let user_uuid: Uuid = serde_json::from_str(&data.get_cache_key(token.to_string()).await?)?; + pub async fn get(app_state: &AppState, token: String) -> Result { + let user_uuid: Uuid = + serde_json::from_str(&app_state.get_cache_key(token.to_string()).await?)?; let password_reset_token = serde_json::from_str( - &data + &app_state .get_cache_key(format!("{user_uuid}_password_reset")) .await?, )?; @@ -36,15 +37,15 @@ impl PasswordResetToken { } pub async fn get_with_identifier( - data: &Data, + app_state: &AppState, identifier: String, ) -> Result { - let mut conn = data.pool.get().await?; + let mut conn = app_state.pool.get().await?; let user_uuid = user_uuid_from_identifier(&mut conn, &identifier).await?; let password_reset_token = serde_json::from_str( - &data + &app_state .get_cache_key(format!("{user_uuid}_password_reset")) .await?, )?; @@ -53,14 +54,14 @@ impl PasswordResetToken { } #[allow(clippy::new_ret_no_self)] - pub async fn new(data: &Data, identifier: String) -> Result<(), Error> { + pub async fn new(app_state: &AppState, identifier: String) -> Result<(), Error> { let token = generate_token::<32>()?; - let mut conn = data.pool.get().await?; + let mut conn = app_state.pool.get().await?; let user_uuid = user_uuid_from_identifier(&mut conn, &identifier).await?; - global_checks(data, user_uuid).await?; + global_checks(app_state, user_uuid).await?; use users::dsl as udsl; let (username, email_address): (String, String) = udsl::users @@ -75,34 +76,37 @@ impl PasswordResetToken { created_at: Utc::now(), }; - data.set_cache_key( - format!("{user_uuid}_password_reset"), - password_reset_token, - 86400, - ) - .await?; - data.set_cache_key(token.clone(), user_uuid, 86400).await?; + app_state + .set_cache_key( + format!("{user_uuid}_password_reset"), + password_reset_token, + 86400, + ) + .await?; + app_state + .set_cache_key(token.clone(), user_uuid, 86400) + .await?; - let mut reset_endpoint = data.config.web.frontend_url.join("reset-password")?; + let mut reset_endpoint = app_state.config.web.frontend_url.join("reset-password")?; reset_endpoint.set_query(Some(&format!("token={token}"))); - let email = data + let email = app_state .mail_client .message_builder() .to(email_address.parse()?) - .subject(format!("{} Password Reset", data.config.instance.name)) + .subject(format!("{} Password Reset", app_state.config.instance.name)) .multipart(MultiPart::alternative_plain_html( - format!("{} Password Reset\n\nHello, {}!\nSomeone requested a password reset for your Gorb account.\nClick the button below within 24 hours to reset your password.\n\n{}\n\nIf you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.\n\nThanks, The gorb team.", data.config.instance.name, username, reset_endpoint), - format!(r#"

{} Password Reset

Hello, {}!

Someone requested a password reset for your Gorb account.

Click the button below within 24 hours to reset your password.

RESET PASSWORD

If you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.

"#, data.config.instance.name, username, reset_endpoint) + format!("{} Password Reset\n\nHello, {}!\nSomeone requested a password reset for your Gorb account.\nClick the button below within 24 hours to reset your password.\n\n{}\n\nIf you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.\n\nThanks, The gorb team.", app_state.config.instance.name, username, reset_endpoint), + format!(r#"

{} Password Reset

Hello, {}!

Someone requested a password reset for your Gorb account.

Click the button below within 24 hours to reset your password.

RESET PASSWORD

If you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.

"#, app_state.config.instance.name, username, reset_endpoint) ))?; - data.mail_client.send_mail(email).await?; + app_state.mail_client.send_mail(email).await?; Ok(()) } - pub async fn set_password(&self, data: &Data, password: String) -> Result<(), Error> { + pub async fn set_password(&self, app_state: &AppState, password: String) -> Result<(), Error> { if !PASSWORD_REGEX.is_match(&password) { return Err(Error::BadRequest( "Please provide a valid password".to_string(), @@ -111,12 +115,12 @@ impl PasswordResetToken { let salt = SaltString::generate(&mut OsRng); - let hashed_password = data + let hashed_password = app_state .argon2 .hash_password(password.as_bytes(), &salt) .map_err(|e| Error::PasswordHashError(e.to_string()))?; - let mut conn = data.pool.get().await?; + let mut conn = app_state.pool.get().await?; use users::dsl; update(users::table) @@ -131,27 +135,28 @@ impl PasswordResetToken { .get_result(&mut conn) .await?; - let login_page = data.config.web.frontend_url.join("login")?; + let login_page = app_state.config.web.frontend_url.join("login")?; - let email = data + let email = app_state .mail_client .message_builder() .to(email_address.parse()?) - .subject(format!("Your {} Password has been Reset", data.config.instance.name)) + .subject(format!("Your {} Password has been Reset", app_state.config.instance.name)) .multipart(MultiPart::alternative_plain_html( - format!("{} Password Reset Confirmation\n\nHello, {}!\nYour password has been successfully reset for your Gorb account.\nIf you did not initiate this change, please click the link below to reset your password immediately.\n\n{}\n\nThanks, The gorb team.", data.config.instance.name, username, login_page), - format!(r#"

{} Password Reset Confirmation

Hello, {}!

Your password has been successfully reset for your Gorb account.

If you did not initiate this change, please click the button below to reset your password immediately.

RESET PASSWORD
"#, data.config.instance.name, username, login_page) + format!("{} Password Reset Confirmation\n\nHello, {}!\nYour password has been successfully reset for your Gorb account.\nIf you did not initiate this change, please click the link below to reset your password immediately.\n\n{}\n\nThanks, The gorb team.", app_state.config.instance.name, username, login_page), + format!(r#"

{} Password Reset Confirmation

Hello, {}!

Your password has been successfully reset for your Gorb account.

If you did not initiate this change, please click the button below to reset your password immediately.

RESET PASSWORD
"#, app_state.config.instance.name, username, login_page) ))?; - data.mail_client.send_mail(email).await?; + app_state.mail_client.send_mail(email).await?; - self.delete(data).await + self.delete(app_state).await } - pub async fn delete(&self, data: &Data) -> Result<(), Error> { - data.del_cache_key(format!("{}_password_reset", &self.user_uuid)) + pub async fn delete(&self, app_state: &AppState) -> Result<(), Error> { + app_state + .del_cache_key(format!("{}_password_reset", &self.user_uuid)) .await?; - data.del_cache_key(self.token.to_string()).await?; + app_state.del_cache_key(self.token.to_string()).await?; Ok(()) } diff --git a/src/objects/role.rs b/src/objects/role.rs index 68e9c27..ea70686 100644 --- a/src/objects/role.rs +++ b/src/objects/role.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ - Conn, Data, + AppState, Conn, error::Error, schema::{role_members, roles}, utils::order_by_is_above, @@ -74,12 +74,18 @@ impl Role { Ok(roles) } - pub async fn fetch_from_member(data: &Data, member_uuid: Uuid) -> Result, Error> { - if let Ok(roles) = data.get_cache_key(format!("{member_uuid}_roles")).await { + pub async fn fetch_from_member( + app_state: &AppState, + member_uuid: Uuid, + ) -> Result, Error> { + if let Ok(roles) = app_state + .get_cache_key(format!("{member_uuid}_roles")) + .await + { return Ok(serde_json::from_str(&roles)?); } - let mut conn = data.pool.get().await?; + let mut conn = app_state.pool.get().await?; use role_members::dsl; let role_memberships: Vec = load_or_empty( @@ -96,7 +102,8 @@ impl Role { roles.push(membership.fetch_role(&mut conn).await?); } - data.set_cache_key(format!("{member_uuid}_roles"), roles.clone(), 300) + app_state + .set_cache_key(format!("{member_uuid}_roles"), roles.clone(), 300) .await?; Ok(roles) diff --git a/src/objects/user.rs b/src/objects/user.rs index 8e42351..c1f164d 100644 --- a/src/objects/user.rs +++ b/src/objects/user.rs @@ -4,7 +4,7 @@ use diesel_async::RunQueryDsl; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::{Conn, Data, error::Error, objects::Me, schema::users}; +use crate::{AppState, Conn, error::Error, objects::Me, schema::users}; use super::load_or_empty; @@ -46,10 +46,10 @@ pub struct User { } impl User { - pub async fn fetch_one(data: &Data, user_uuid: Uuid) -> Result { - let mut conn = data.pool.get().await?; + pub async fn fetch_one(app_state: &AppState, user_uuid: Uuid) -> Result { + let mut conn = app_state.pool.get().await?; - if let Ok(cache_hit) = data.get_cache_key(user_uuid.to_string()).await { + if let Ok(cache_hit) = app_state.get_cache_key(user_uuid.to_string()).await { return Ok(serde_json::from_str(&cache_hit)?); } @@ -62,20 +62,21 @@ impl User { let user = user_builder.build(); - data.set_cache_key(user_uuid.to_string(), user.clone(), 1800) + app_state + .set_cache_key(user_uuid.to_string(), user.clone(), 1800) .await?; Ok(user) } pub async fn fetch_one_with_friendship( - data: &Data, + app_state: &AppState, me: &Me, user_uuid: Uuid, ) -> Result { - let mut conn = data.pool.get().await?; + let mut conn = app_state.pool.get().await?; - let mut user = Self::fetch_one(data, user_uuid).await?; + let mut user = Self::fetch_one(app_state, user_uuid).await?; if let Some(friend) = me.friends_with(&mut conn, user_uuid).await? { user.friends_since = Some(friend.accepted_at); diff --git a/src/socket.rs b/src/socket.rs new file mode 100644 index 0000000..e00a7c0 --- /dev/null +++ b/src/socket.rs @@ -0,0 +1,26 @@ +use std::sync::Arc; + +use log::info; +use rmpv::Value; +use socketioxide::{ + extract::{AckSender, Data, SocketRef, State}, +}; + +use crate::AppState; + +pub async fn on_connect(State(app_state): State>, socket: SocketRef, Data(data): Data) { + socket.emit("auth", &data).ok(); + + socket.on("message", async |socket: SocketRef, Data::(data)| { + info!("{}", data); + socket.emit("message-back", &data).ok(); + }); + + socket.on( + "message-with-ack", + async |Data::(data), ack: AckSender| { + info!("{}", data); + ack.send(&data).ok(); + }, + ); +} diff --git a/src/utils.rs b/src/utils.rs index 072143f..6083188 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,10 +1,7 @@ use std::sync::LazyLock; -use actix_web::{ - cookie::{Cookie, SameSite, time::Duration}, - http::header::HeaderMap, - web::BytesMut, -}; +use axum::body::Bytes; +use axum_extra::extract::cookie::{Cookie, SameSite}; use bindet::FileType; use diesel::{ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; @@ -13,10 +10,11 @@ use hex::encode; use redis::RedisError; use regex::Regex; use serde::Serialize; +use time::Duration; use uuid::Uuid; use crate::{ - Conn, Data, + AppState, Conn, config::Config, error::Error, objects::{HasIsAbove, HasUuid}, @@ -33,86 +31,26 @@ pub static USERNAME_REGEX: LazyLock = pub static CHANNEL_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"^[a-z0-9_.-]+$").unwrap()); -// Password is expected to be hashed using SHA3-384 pub static PASSWORD_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"[0-9a-f]{96}").unwrap()); -pub fn get_auth_header(headers: &HeaderMap) -> Result<&str, Error> { - let auth_token = headers.get(actix_web::http::header::AUTHORIZATION); - - if auth_token.is_none() { - return Err(Error::Unauthorized( - "No authorization header provided".to_string(), - )); - } - - let auth_raw = auth_token.unwrap().to_str()?; - - let mut auth = auth_raw.split_whitespace(); - - let auth_type = auth.next(); - - let auth_value = auth.next(); - - if auth_type.is_none() { - return Err(Error::BadRequest( - "Authorization header is empty".to_string(), - )); - } else if auth_type.is_some_and(|at| at != "Bearer") { - return Err(Error::BadRequest( - "Only token auth is supported".to_string(), - )); - } - - if auth_value.is_none() { - return Err(Error::BadRequest("No token provided".to_string())); - } - - Ok(auth_value.unwrap()) -} - -pub fn get_ws_protocol_header(headers: &HeaderMap) -> Result<&str, Error> { - let auth_token = headers.get(actix_web::http::header::SEC_WEBSOCKET_PROTOCOL); - - if auth_token.is_none() { - return Err(Error::Unauthorized( - "No authorization header provided".to_string(), - )); - } - - let auth_raw = auth_token.unwrap().to_str()?; - - let mut auth = auth_raw.split_whitespace(); - - let response_proto = auth.next(); - - let auth_value = auth.next(); - - if response_proto.is_none() { - return Err(Error::BadRequest( - "Sec-WebSocket-Protocol header is empty".to_string(), - )); - } else if response_proto.is_some_and(|rp| rp != "Authorization,") { - return Err(Error::BadRequest( - "First protocol should be Authorization".to_string(), - )); - } - - if auth_value.is_none() { - return Err(Error::BadRequest("No token provided".to_string())); - } - - Ok(auth_value.unwrap()) -} - -pub fn new_refresh_token_cookie(config: &Config, refresh_token: String) -> Cookie<'static> { - Cookie::build("refresh_token", refresh_token) +pub fn new_refresh_token_cookie(config: &Config, refresh_token: String) -> Cookie { + Cookie::build(("refresh_token", refresh_token)) .http_only(true) .secure(true) .same_site(SameSite::None) - //.domain(config.web.backend_url.domain().unwrap().to_string()) .path(config.web.backend_url.path().to_string()) .max_age(Duration::days(30)) - .finish() + .build() +} + +pub fn new_access_token_cookie(config: &Config, access_token: String) -> Cookie { + Cookie::build(("access_token", access_token)) + .http_only(false) + .secure(true) + .same_site(SameSite::None) + .path(config.web.backend_url.path().to_string()) + .max_age(Duration::hours(1)) + .build() } pub fn generate_token() -> Result { @@ -121,7 +59,7 @@ pub fn generate_token() -> Result { Ok(encode(buf)) } -pub fn image_check(icon: BytesMut) -> Result { +pub fn image_check(icon: Bytes) -> Result { let buf = std::io::Cursor::new(icon); let detect = bindet::detect(buf).map_err(|e| e.kind()); @@ -168,10 +106,7 @@ pub async fn user_uuid_from_identifier( } } -pub async fn user_uuid_from_username( - conn: &mut Conn, - username: &String, -) -> Result { +pub async fn user_uuid_from_username(conn: &mut Conn, username: &String) -> Result { if USERNAME_REGEX.is_match(username) { use users::dsl; let user_uuid = dsl::users @@ -188,9 +123,9 @@ pub async fn user_uuid_from_username( } } -pub async fn global_checks(data: &Data, user_uuid: Uuid) -> Result<(), Error> { - if data.config.instance.require_email_verification { - let mut conn = data.pool.get().await?; +pub async fn global_checks(app_state: &AppState, user_uuid: Uuid) -> Result<(), Error> { + if app_state.config.instance.require_email_verification { + let mut conn = app_state.pool.get().await?; use users::dsl; let email_verified: bool = dsl::users @@ -234,7 +169,7 @@ where Ok(ordered) } -impl Data { +impl AppState { pub async fn set_cache_key( &self, key: String, From c9dd66dd809d641953144f2581e80e0392bd37d6 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 17 Jul 2025 15:52:11 +0200 Subject: [PATCH 095/160] ci: add staging images --- .woodpecker/build-and-publish.yml | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/.woodpecker/build-and-publish.yml b/.woodpecker/build-and-publish.yml index 57f2761..9836311 100644 --- a/.woodpecker/build-and-publish.yml +++ b/.woodpecker/build-and-publish.yml @@ -1,12 +1,12 @@ -when: - - event: push - branch: main - steps: - name: build-x86_64 image: rust:bookworm commands: - cargo build --release + when: + - event: push + - event: pull_request + - name: build-arm64 image: rust:bookworm commands: @@ -18,6 +18,10 @@ steps: CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc PKG_CONFIG_ALLOW_CROSS: 1 PKG_CONFIG_PATH: /usr/aarch64-linux-gnu/lib/pkgconfig + when: + - event: push + - event: pull_request + - name: container-build-and-publish image: docker commands: @@ -28,3 +32,20 @@ steps: from_secret: docker_password volumes: - /var/run/podman/podman.sock:/var/run/docker.sock + when: + - branch: main + event: push + + - name: container-build-and-publish (staging) + image: docker + commands: + - docker login --username radical --password $PASSWORD git.gorb.app + - docker buildx build --platform linux/amd64,linux/arm64 --rm --push -t git.gorb.app/gorb/backend:staging . + environment: + PASSWORD: + from_secret: docker_password + volumes: + - /var/run/podman/podman.sock:/var/run/docker.sock + when: + - branch: staging + event: push From 1946080716a72f733ac1aa424cbc1c869eb21429 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 17 Jul 2025 16:07:09 +0200 Subject: [PATCH 096/160] ci: remove parentheses from name --- .woodpecker/build-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.woodpecker/build-and-publish.yml b/.woodpecker/build-and-publish.yml index 9836311..4e263a9 100644 --- a/.woodpecker/build-and-publish.yml +++ b/.woodpecker/build-and-publish.yml @@ -36,7 +36,7 @@ steps: - branch: main event: push - - name: container-build-and-publish (staging) + - name: container-build-and-publish-staging image: docker commands: - docker login --username radical --password $PASSWORD git.gorb.app From 9a0ebf2b2fce0d624945fff518560cf65695fd1e Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 17 Jul 2025 16:48:34 +0200 Subject: [PATCH 097/160] fix: use merge instead of nesting --- src/api/mod.rs | 6 +++--- src/main.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index e4c3f2e..a00d1e5 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -9,8 +9,8 @@ use crate::AppState; mod v1; mod versions; -pub fn router() -> Router> { +pub fn router(path: &str) -> Router> { Router::new() - .route("/versions", get(versions::versions)) - .nest("/v1", v1::router()) + .route(&format!("{path}/versions"), get(versions::versions)) + .nest(&format!("{path}/v1"), v1::router()) } diff --git a/src/main.rs b/src/main.rs index 6bb2be3..15bae09 100644 --- a/src/main.rs +++ b/src/main.rs @@ -141,7 +141,7 @@ async fn main() -> Result<(), Error> { // build our application with a route let app = Router::new() // `GET /` goes to `root` - .nest(web.backend_url.path(), api::router()) + .merge(api::router(web.backend_url.path().trim_end_matches("/"))) .with_state(app_state) .layer(cors) .layer(socket_io); From 8f53c9f718e6be24075427a95e42e6e17ca441f6 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 17 Jul 2025 21:34:35 +0200 Subject: [PATCH 098/160] fix: try to fix up cors Login still not working, unsure of where failure point is --- src/main.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index 15bae09..9624d18 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ use error::Error; use objects::MailClient; use socketioxide::SocketIo; use std::{sync::Arc, time::SystemTime}; -use tower_http::cors::{Any, CorsLayer}; +use tower_http::cors::{AllowHeaders, AllowMethods, AllowOrigin, CorsLayer}; mod config; use config::{Config, ConfigBuilder}; use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations}; @@ -128,11 +128,21 @@ async fn main() -> Result<(), Error> { let cors = CorsLayer::new() // Allow any origin (equivalent to allowed_origin_fn returning true) - .allow_origin(Any) + .allow_origin(AllowOrigin::predicate(|_origin, _request_head| { + true + })) // Allow any method - .allow_methods(Any) + .allow_methods(AllowMethods::mirror_request()) // Allow any headers - .allow_headers(Any); + .allow_headers(AllowHeaders::mirror_request()) + /* + vec![ + "content-type".parse().unwrap(), + "authorization".parse().unwrap(), + ] + */ + // Allow credentials + .allow_credentials(true); let (socket_io, io) = SocketIo::builder().with_state(app_state.clone()).build_layer(); From d67a7ce0ca8afbd02a8768a63e24dff84ed762f2 Mon Sep 17 00:00:00 2001 From: Radical Date: Fri, 18 Jul 2025 12:00:28 +0200 Subject: [PATCH 099/160] fix: try explicitly setting methods and headers --- src/main.rs | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/main.rs b/src/main.rs index 9624d18..baf4a61 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ use argon2::Argon2; -use axum::Router; +use axum::{http::header, Router}; use clap::Parser; use diesel_async::pooled_connection::AsyncDieselConnectionManager; use diesel_async::pooled_connection::deadpool::Pool; @@ -131,16 +131,22 @@ async fn main() -> Result<(), Error> { .allow_origin(AllowOrigin::predicate(|_origin, _request_head| { true })) - // Allow any method - .allow_methods(AllowMethods::mirror_request()) - // Allow any headers - .allow_headers(AllowHeaders::mirror_request()) - /* - vec![ - "content-type".parse().unwrap(), - "authorization".parse().unwrap(), - ] - */ + .allow_methods(AllowMethods::list([ + "GET".parse().unwrap(), + "POST".parse().unwrap(), + "PUT".parse().unwrap(), + "PATCH".parse().unwrap(), + "DELETE".parse().unwrap(), + "OPTIONS".parse().unwrap(), + ])) + .allow_headers(AllowHeaders::list([ + header::AUTHORIZATION, + header::CONTENT_TYPE, + header::ORIGIN, + header::ACCEPT, + header::COOKIE, + "x-requested-with".parse().unwrap(), + ])) // Allow credentials .allow_credentials(true); From 2fbf41ba8cdaefee91ef252de04bb2aefdff2dce Mon Sep 17 00:00:00 2001 From: Radical Date: Sat, 19 Jul 2025 19:10:36 +0200 Subject: [PATCH 100/160] fix: use .append() and not Set-Cookie2 web dev is too confusing.. --- src/api/v1/auth/login.rs | 6 +++--- src/api/v1/auth/logout.rs | 2 +- src/api/v1/auth/refresh.rs | 26 +++++++++----------------- src/api/v1/auth/register.rs | 2 +- 4 files changed, 14 insertions(+), 22 deletions(-) diff --git a/src/api/v1/auth/login.rs b/src/api/v1/auth/login.rs index 2391fdf..7779564 100644 --- a/src/api/v1/auth/login.rs +++ b/src/api/v1/auth/login.rs @@ -95,15 +95,15 @@ pub async fn response( let mut response = StatusCode::OK.into_response(); - response.headers_mut().insert( + response.headers_mut().append( "Set-Cookie", HeaderValue::from_str( &new_refresh_token_cookie(&app_state.config, refresh_token).to_string(), )?, ); - response.headers_mut().insert( - "Set-Cookie2", + response.headers_mut().append( + "Set-Cookie", HeaderValue::from_str( &new_access_token_cookie(&app_state.config, access_token).to_string(), )?, diff --git a/src/api/v1/auth/logout.rs b/src/api/v1/auth/logout.rs index 6e5e98d..906afcc 100644 --- a/src/api/v1/auth/logout.rs +++ b/src/api/v1/auth/logout.rs @@ -68,7 +68,7 @@ pub async fn res( cookie.make_removal(); response .headers_mut() - .append("Set-Cookie2", HeaderValue::from_str(&cookie.to_string())?); + .append("Set-Cookie", HeaderValue::from_str(&cookie.to_string())?); } Ok(response) diff --git a/src/api/v1/auth/refresh.rs b/src/api/v1/auth/refresh.rs index 2a7e611..b104a8e 100644 --- a/src/api/v1/auth/refresh.rs +++ b/src/api/v1/auth/refresh.rs @@ -71,7 +71,7 @@ pub async fn post( cookie.make_removal(); response .headers_mut() - .append("Set-Cookie2", HeaderValue::from_str(&cookie.to_string())?); + .append("Set-Cookie", HeaderValue::from_str(&cookie.to_string())?); } return Ok(response); @@ -119,21 +119,13 @@ pub async fn post( .execute(&mut conn) .await?; - if response.headers().get("Set-Cookie").is_some() { - response.headers_mut().append( - "Set-Cookie2", - HeaderValue::from_str( - &new_access_token_cookie(&app_state.config, access_token).to_string(), - )?, - ); - } else { - response.headers_mut().append( - "Set-Cookie", - HeaderValue::from_str( - &new_access_token_cookie(&app_state.config, access_token).to_string(), - )?, - ); - } + + response.headers_mut().append( + "Set-Cookie", + HeaderValue::from_str( + &new_access_token_cookie(&app_state.config, access_token).to_string(), + )?, + ); return Ok(response); } @@ -151,7 +143,7 @@ pub async fn post( cookie.make_removal(); response .headers_mut() - .append("Set-Cookie2", HeaderValue::from_str(&cookie.to_string())?); + .append("Set-Cookie", HeaderValue::from_str(&cookie.to_string())?); } Ok(response) diff --git a/src/api/v1/auth/register.rs b/src/api/v1/auth/register.rs index 06b63ca..237f1e0 100644 --- a/src/api/v1/auth/register.rs +++ b/src/api/v1/auth/register.rs @@ -169,7 +169,7 @@ pub async fn post( )?, ); response.headers_mut().append( - "Set-Cookie2", + "Set-Cookie", HeaderValue::from_str( &new_access_token_cookie(&app_state.config, access_token).to_string(), )?, From 252b9a3dc652fd28d5bbb8a4359121433f637adc Mon Sep 17 00:00:00 2001 From: Radical Date: Sat, 19 Jul 2025 23:03:23 +0200 Subject: [PATCH 101/160] fix: add more cors shit can someone please just make cors disappear? god i hate this shit. --- src/main.rs | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/main.rs b/src/main.rs index baf4a61..73110c4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ use argon2::Argon2; -use axum::{http::header, Router}; +use axum::{http::{header, Method}, Router}; use clap::Parser; use diesel_async::pooled_connection::AsyncDieselConnectionManager; use diesel_async::pooled_connection::deadpool::Pool; @@ -131,22 +131,28 @@ async fn main() -> Result<(), Error> { .allow_origin(AllowOrigin::predicate(|_origin, _request_head| { true })) - .allow_methods(AllowMethods::list([ - "GET".parse().unwrap(), - "POST".parse().unwrap(), - "PUT".parse().unwrap(), - "PATCH".parse().unwrap(), - "DELETE".parse().unwrap(), - "OPTIONS".parse().unwrap(), - ])) - .allow_headers(AllowHeaders::list([ + .allow_methods(vec![ + Method::GET, + Method::POST, + Method::PUT, + Method::DELETE, + Method::HEAD, + Method::OPTIONS, + Method::CONNECT, + Method::PATCH, + Method::TRACE, + ]) + .allow_headers(vec![ + header::ACCEPT, + header::ACCEPT_LANGUAGE, header::AUTHORIZATION, + header::CONTENT_LANGUAGE, header::CONTENT_TYPE, header::ORIGIN, header::ACCEPT, header::COOKIE, "x-requested-with".parse().unwrap(), - ])) + ]) // Allow credentials .allow_credentials(true); From d2fec66ddbcc3a5739aec630322c7ee2e019d7e3 Mon Sep 17 00:00:00 2001 From: Radical Date: Sat, 19 Jul 2025 23:20:16 +0200 Subject: [PATCH 102/160] fix: try not setting path on access token --- src/api/v1/auth/login.rs | 2 +- src/api/v1/auth/refresh.rs | 2 +- src/api/v1/auth/register.rs | 2 +- src/utils.rs | 3 +-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/api/v1/auth/login.rs b/src/api/v1/auth/login.rs index 7779564..995e299 100644 --- a/src/api/v1/auth/login.rs +++ b/src/api/v1/auth/login.rs @@ -105,7 +105,7 @@ pub async fn response( response.headers_mut().append( "Set-Cookie", HeaderValue::from_str( - &new_access_token_cookie(&app_state.config, access_token).to_string(), + &new_access_token_cookie(access_token).to_string(), )?, ); diff --git a/src/api/v1/auth/refresh.rs b/src/api/v1/auth/refresh.rs index b104a8e..d6bc3a9 100644 --- a/src/api/v1/auth/refresh.rs +++ b/src/api/v1/auth/refresh.rs @@ -123,7 +123,7 @@ pub async fn post( response.headers_mut().append( "Set-Cookie", HeaderValue::from_str( - &new_access_token_cookie(&app_state.config, access_token).to_string(), + &new_access_token_cookie(access_token).to_string(), )?, ); diff --git a/src/api/v1/auth/register.rs b/src/api/v1/auth/register.rs index 237f1e0..9f05b04 100644 --- a/src/api/v1/auth/register.rs +++ b/src/api/v1/auth/register.rs @@ -171,7 +171,7 @@ pub async fn post( response.headers_mut().append( "Set-Cookie", HeaderValue::from_str( - &new_access_token_cookie(&app_state.config, access_token).to_string(), + &new_access_token_cookie(access_token).to_string(), )?, ); diff --git a/src/utils.rs b/src/utils.rs index 6083188..7cda5b3 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -43,12 +43,11 @@ pub fn new_refresh_token_cookie(config: &Config, refresh_token: String) -> Cooki .build() } -pub fn new_access_token_cookie(config: &Config, access_token: String) -> Cookie { +pub fn new_access_token_cookie<'cookie>(access_token: String) -> Cookie<'cookie> { Cookie::build(("access_token", access_token)) .http_only(false) .secure(true) .same_site(SameSite::None) - .path(config.web.backend_url.path().to_string()) .max_age(Duration::hours(1)) .build() } From 9bf435b5350cf179318fba400d51337a62a62954 Mon Sep 17 00:00:00 2001 From: Radical Date: Sat, 19 Jul 2025 23:39:56 +0200 Subject: [PATCH 103/160] fix: revert changes to access_token made during refactor --- src/api/v1/auth/login.rs | 12 +++-------- src/api/v1/auth/mod.rs | 8 +++++++ src/api/v1/auth/refresh.rs | 42 +++++++++---------------------------- src/api/v1/auth/register.rs | 11 +++------- src/main.rs | 2 +- src/utils.rs | 9 -------- 6 files changed, 25 insertions(+), 59 deletions(-) diff --git a/src/api/v1/auth/login.rs b/src/api/v1/auth/login.rs index 995e299..d5cba95 100644 --- a/src/api/v1/auth/login.rs +++ b/src/api/v1/auth/login.rs @@ -14,12 +14,13 @@ use diesel::{ExpressionMethods, QueryDsl, dsl::insert_into}; use diesel_async::RunQueryDsl; use serde::Deserialize; +use super::Response; use crate::{ AppState, error::Error, schema::*, utils::{ - PASSWORD_REGEX, generate_token, new_access_token_cookie, new_refresh_token_cookie, + PASSWORD_REGEX, generate_token, new_refresh_token_cookie, user_uuid_from_identifier, }, }; @@ -93,7 +94,7 @@ pub async fn response( .execute(&mut conn) .await?; - let mut response = StatusCode::OK.into_response(); + let mut response = (StatusCode::OK, Json(Response { access_token })).into_response(); response.headers_mut().append( "Set-Cookie", @@ -102,12 +103,5 @@ pub async fn response( )?, ); - response.headers_mut().append( - "Set-Cookie", - HeaderValue::from_str( - &new_access_token_cookie(access_token).to_string(), - )?, - ); - Ok(response) } diff --git a/src/api/v1/auth/mod.rs b/src/api/v1/auth/mod.rs index 88be220..59d7a8e 100644 --- a/src/api/v1/auth/mod.rs +++ b/src/api/v1/auth/mod.rs @@ -9,6 +9,7 @@ use axum::{ }; use diesel::{ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; +use serde::Serialize; use uuid::Uuid; use crate::{AppState, Conn, error::Error, schema::access_tokens::dsl}; @@ -22,6 +23,13 @@ mod reset_password; mod revoke; mod verify_email; + +#[derive(Serialize)] +pub struct Response { + access_token: String, +} + + pub fn router() -> Router> { Router::new() .route("/register", post(register::post)) diff --git a/src/api/v1/auth/refresh.rs b/src/api/v1/auth/refresh.rs index d6bc3a9..e9709ed 100644 --- a/src/api/v1/auth/refresh.rs +++ b/src/api/v1/auth/refresh.rs @@ -1,7 +1,7 @@ use axum::{ extract::State, http::{HeaderValue, StatusCode}, - response::IntoResponse, + response::IntoResponse, Json, }; use axum_extra::extract::CookieJar; use diesel::{ExpressionMethods, QueryDsl, delete, update}; @@ -12,6 +12,7 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; +use super::Response; use crate::{ AppState, error::Error, @@ -19,7 +20,7 @@ use crate::{ access_tokens::{self, dsl}, refresh_tokens::{self, dsl as rdsl}, }, - utils::{generate_token, new_access_token_cookie, new_refresh_token_cookie}, + utils::{generate_token, new_refresh_token_cookie}, }; pub async fn post( @@ -33,9 +34,7 @@ pub async fn post( ))? .to_owned(); - let access_token_cookie = jar.get("access_token"); - - let refresh_token = String::from(refresh_token_cookie.value_trimmed()); + let mut refresh_token = String::from(refresh_token_cookie.value_trimmed()); let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; @@ -66,21 +65,11 @@ pub async fn post( HeaderValue::from_str(&refresh_token_cookie.to_string())?, ); - if let Some(cookie) = access_token_cookie { - let mut cookie = cookie.clone(); - cookie.make_removal(); - response - .headers_mut() - .append("Set-Cookie", HeaderValue::from_str(&cookie.to_string())?); - } - return Ok(response); } let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; - let mut response = StatusCode::OK.into_response(); - if lifetime > 1987200 { let new_refresh_token = generate_token::<32>()?; @@ -94,13 +83,7 @@ pub async fn post( .await { Ok(_) => { - response.headers_mut().append( - "Set-Cookie", - HeaderValue::from_str( - &new_refresh_token_cookie(&app_state.config, new_refresh_token) - .to_string(), - )?, - ); + refresh_token = new_refresh_token; } Err(error) => { error!("{error}"); @@ -119,13 +102,16 @@ pub async fn post( .execute(&mut conn) .await?; - + let mut response = (StatusCode::OK, Json(Response { access_token })).into_response(); + + // TODO: Dont set this when refresh token is unchanged response.headers_mut().append( "Set-Cookie", HeaderValue::from_str( - &new_access_token_cookie(access_token).to_string(), + &new_refresh_token_cookie(&app_state.config, refresh_token).to_string(), )?, ); + return Ok(response); } @@ -138,13 +124,5 @@ pub async fn post( HeaderValue::from_str(&refresh_token_cookie.to_string())?, ); - if let Some(cookie) = access_token_cookie { - let mut cookie = cookie.clone(); - cookie.make_removal(); - response - .headers_mut() - .append("Set-Cookie", HeaderValue::from_str(&cookie.to_string())?); - } - Ok(response) } diff --git a/src/api/v1/auth/register.rs b/src/api/v1/auth/register.rs index 9f05b04..c190821 100644 --- a/src/api/v1/auth/register.rs +++ b/src/api/v1/auth/register.rs @@ -18,6 +18,7 @@ use diesel_async::RunQueryDsl; use serde::{Deserialize, Serialize}; use uuid::Uuid; +use super::Response; use crate::{ AppState, error::Error, @@ -28,7 +29,7 @@ use crate::{ users::{self, dsl as udsl}, }, utils::{ - EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX, generate_token, new_access_token_cookie, + EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX, generate_token, new_refresh_token_cookie, }, }; @@ -160,7 +161,7 @@ pub async fn post( Member::new(&app_state, uuid, initial_guild).await?; } - let mut response = StatusCode::OK.into_response(); + let mut response = (StatusCode::OK, Json(Response {access_token})).into_response(); response.headers_mut().append( "Set-Cookie", @@ -168,12 +169,6 @@ pub async fn post( &new_refresh_token_cookie(&app_state.config, refresh_token).to_string(), )?, ); - response.headers_mut().append( - "Set-Cookie", - HeaderValue::from_str( - &new_access_token_cookie(access_token).to_string(), - )?, - ); return Ok(response); } diff --git a/src/main.rs b/src/main.rs index 73110c4..ab37924 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ use error::Error; use objects::MailClient; use socketioxide::SocketIo; use std::{sync::Arc, time::SystemTime}; -use tower_http::cors::{AllowHeaders, AllowMethods, AllowOrigin, CorsLayer}; +use tower_http::cors::{AllowOrigin, CorsLayer}; mod config; use config::{Config, ConfigBuilder}; use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations}; diff --git a/src/utils.rs b/src/utils.rs index 7cda5b3..0f986a2 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -43,15 +43,6 @@ pub fn new_refresh_token_cookie(config: &Config, refresh_token: String) -> Cooki .build() } -pub fn new_access_token_cookie<'cookie>(access_token: String) -> Cookie<'cookie> { - Cookie::build(("access_token", access_token)) - .http_only(false) - .secure(true) - .same_site(SameSite::None) - .max_age(Duration::hours(1)) - .build() -} - pub fn generate_token() -> Result { let mut buf = [0u8; N]; fill(&mut buf)?; From dada230e08426590c13e1edb1215f4331f9a42fe Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 20 Jul 2025 13:04:08 +0200 Subject: [PATCH 104/160] fix: remove the rest of the leftover code from access_token cookies --- src/api/v1/auth/logout.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/api/v1/auth/logout.rs b/src/api/v1/auth/logout.rs index 906afcc..977d452 100644 --- a/src/api/v1/auth/logout.rs +++ b/src/api/v1/auth/logout.rs @@ -38,8 +38,6 @@ pub async fn res( ))? .to_owned(); - let access_token_cookie = jar.get("access_token"); - let refresh_token = String::from(refresh_token_cookie.value_trimmed()); let mut conn = app_state.pool.get().await?; @@ -63,13 +61,5 @@ pub async fn res( HeaderValue::from_str(&refresh_token_cookie.to_string())?, ); - if let Some(cookie) = access_token_cookie { - let mut cookie = cookie.clone(); - cookie.make_removal(); - response - .headers_mut() - .append("Set-Cookie", HeaderValue::from_str(&cookie.to_string())?); - } - Ok(response) } From 1ad88725bd4f09d45483cc0f7cc575f0f3f30410 Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 20 Jul 2025 14:12:57 +0200 Subject: [PATCH 105/160] feat: use custom middleware for authorization --- src/api/mod.rs | 6 ++--- src/api/v1/auth/devices.rs | 20 ++++----------- src/api/v1/auth/mod.rs | 35 ++++++++++++++++++++------ src/api/v1/auth/revoke.rs | 17 +++---------- src/api/v1/auth/verify_email.rs | 24 +++++------------- src/api/v1/channels/mod.rs | 5 ---- src/api/v1/channels/uuid/messages.rs | 23 +++-------------- src/api/v1/channels/uuid/mod.rs | 36 +++++++-------------------- src/api/v1/guilds/mod.rs | 30 +++++----------------- src/api/v1/guilds/uuid/channels.rs | 31 +++++------------------ src/api/v1/guilds/uuid/invites/mod.rs | 27 ++++++-------------- src/api/v1/guilds/uuid/members.rs | 19 ++++---------- src/api/v1/guilds/uuid/mod.rs | 28 ++++++--------------- src/api/v1/guilds/uuid/roles/mod.rs | 25 ++++++------------- src/api/v1/guilds/uuid/roles/uuid.rs | 19 ++++---------- src/api/v1/invites/id.rs | 20 +++++---------- src/api/v1/me/friends/mod.rs | 29 ++++++--------------- src/api/v1/me/friends/uuid.rs | 16 ++++-------- src/api/v1/me/guilds.rs | 17 +++++-------- src/api/v1/me/mod.rs | 29 ++++++--------------- src/api/v1/mod.rs | 16 +++++++----- src/api/v1/users/mod.rs | 25 ++++--------------- src/api/v1/users/uuid.rs | 23 +++-------------- src/main.rs | 2 +- 24 files changed, 157 insertions(+), 365 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index a00d1e5..988ee45 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -2,15 +2,15 @@ use std::sync::Arc; -use axum::{Router, routing::get}; +use axum::{routing::get, Router}; use crate::AppState; mod v1; mod versions; -pub fn router(path: &str) -> Router> { +pub fn router(path: &str, app_state: Arc) -> Router> { Router::new() .route(&format!("{path}/versions"), get(versions::versions)) - .nest(&format!("{path}/v1"), v1::router()) + .nest(&format!("{path}/v1"), v1::router(app_state)) } diff --git a/src/api/v1/auth/devices.rs b/src/api/v1/auth/devices.rs index a3c12d1..336a52f 100644 --- a/src/api/v1/auth/devices.rs +++ b/src/api/v1/auth/devices.rs @@ -2,20 +2,14 @@ use std::sync::Arc; -use axum::{Json, extract::State, http::StatusCode, response::IntoResponse}; -use axum_extra::{ - TypedHeader, - headers::{Authorization, authorization::Bearer}, -}; +use axum::{extract::State, http::StatusCode, response::IntoResponse, Extension, Json}; use diesel::{ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper}; use diesel_async::RunQueryDsl; use serde::Serialize; +use uuid::Uuid; use crate::{ - AppState, - api::v1::auth::check_access_token, - error::Error, - schema::refresh_tokens::{self, dsl}, + api::v1::auth::CurrentUser, error::Error, schema::refresh_tokens::{self, dsl}, AppState }; #[derive(Serialize, Selectable, Queryable)] @@ -42,16 +36,12 @@ struct Device { /// ``` pub async fn get( State(app_state): State>, - TypedHeader(auth): TypedHeader>, + Extension(CurrentUser(uuid)): Extension>, ) -> Result { - let mut conn = app_state.pool.get().await?; - - let uuid = check_access_token(auth.token(), &mut conn).await?; - let devices: Vec = dsl::refresh_tokens .filter(dsl::uuid.eq(uuid)) .select(Device::as_select()) - .get_results(&mut conn) + .get_results(&mut app_state.pool.get().await?) .await?; Ok((StatusCode::OK, Json(devices))) diff --git a/src/api/v1/auth/mod.rs b/src/api/v1/auth/mod.rs index 59d7a8e..899d6d2 100644 --- a/src/api/v1/auth/mod.rs +++ b/src/api/v1/auth/mod.rs @@ -4,9 +4,9 @@ use std::{ }; use axum::{ - Router, - routing::{delete, get, post}, + extract::{Request, State}, middleware::{from_fn_with_state, Next}, response::IntoResponse, routing::{delete, get, post}, Router }; +use axum_extra::{headers::{authorization::Bearer, Authorization}, TypedHeader}; use diesel::{ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; use serde::Serialize; @@ -30,18 +30,22 @@ pub struct Response { } -pub fn router() -> Router> { +pub fn router(app_state: Arc) -> Router> { + let router_with_auth = Router::new() + .route("/verify-email", get(verify_email::get)) + .route("/verify-email", post(verify_email::post)) + .route("/revoke", post(revoke::post)) + .route("/devices", get(devices::get)) + .layer(from_fn_with_state(app_state, CurrentUser::check_auth_layer)); + Router::new() .route("/register", post(register::post)) .route("/login", post(login::response)) .route("/logout", delete(logout::res)) .route("/refresh", post(refresh::post)) - .route("/revoke", post(revoke::post)) - .route("/verify-email", get(verify_email::get)) - .route("/verify-email", post(verify_email::post)) .route("/reset-password", get(reset_password::get)) .route("/reset-password", post(reset_password::post)) - .route("/devices", get(devices::get)) + .merge(router_with_auth) } pub async fn check_access_token(access_token: &str, conn: &mut Conn) -> Result { @@ -68,3 +72,20 @@ pub async fn check_access_token(access_token: &str, conn: &mut Conn) -> Result(pub Uuid); + +impl CurrentUser { + pub async fn check_auth_layer( + State(app_state): State>, + TypedHeader(auth): TypedHeader>, + mut req: Request, + next: Next + ) -> Result { + let current_user = CurrentUser(check_access_token(auth.token(), &mut app_state.pool.get().await?).await?); + + req.extensions_mut().insert(current_user); + Ok(next.run(req).await) + } +} diff --git a/src/api/v1/auth/revoke.rs b/src/api/v1/auth/revoke.rs index 50aa6d2..b59172e 100644 --- a/src/api/v1/auth/revoke.rs +++ b/src/api/v1/auth/revoke.rs @@ -1,21 +1,14 @@ use std::sync::Arc; use argon2::{PasswordHash, PasswordVerifier}; -use axum::{Json, extract::State, http::StatusCode, response::IntoResponse}; -use axum_extra::{ - TypedHeader, - headers::authorization::{Authorization, Bearer}, -}; +use axum::{extract::State, http::StatusCode, response::IntoResponse, Extension, Json}; use diesel::{ExpressionMethods, QueryDsl, delete}; use diesel_async::RunQueryDsl; use serde::Deserialize; +use uuid::Uuid; use crate::{ - AppState, - api::v1::auth::check_access_token, - error::Error, - schema::refresh_tokens::{self, dsl as rdsl}, - schema::users::dsl as udsl, + api::v1::auth::CurrentUser, error::Error, schema::{refresh_tokens::{self, dsl as rdsl}, users::dsl as udsl}, AppState }; #[derive(Deserialize)] @@ -28,13 +21,11 @@ pub struct RevokeRequest { #[axum::debug_handler] pub async fn post( State(app_state): State>, - TypedHeader(auth): TypedHeader>, + Extension(CurrentUser(uuid)): Extension>, Json(revoke_request): Json, ) -> Result { let mut conn = app_state.pool.get().await?; - let uuid = check_access_token(auth.token(), &mut conn).await?; - let database_password: String = udsl::users .filter(udsl::uuid.eq(uuid)) .select(udsl::password) diff --git a/src/api/v1/auth/verify_email.rs b/src/api/v1/auth/verify_email.rs index 28aa1ab..1270966 100644 --- a/src/api/v1/auth/verify_email.rs +++ b/src/api/v1/auth/verify_email.rs @@ -5,20 +5,14 @@ use std::sync::Arc; use axum::{ extract::{Query, State}, http::StatusCode, - response::IntoResponse, -}; -use axum_extra::{ - TypedHeader, - headers::{Authorization, authorization::Bearer}, + response::IntoResponse, Extension, }; use chrono::{Duration, Utc}; use serde::Deserialize; +use uuid::Uuid; use crate::{ - AppState, - api::v1::auth::check_access_token, - error::Error, - objects::{EmailToken, Me}, + api::v1::auth::CurrentUser, error::Error, objects::{EmailToken, Me}, AppState }; #[derive(Deserialize)] @@ -47,12 +41,10 @@ pub struct QueryParams { pub async fn get( State(app_state): State>, Query(query): Query, - TypedHeader(auth): TypedHeader>, + Extension(CurrentUser(uuid)): Extension> ) -> Result { let mut conn = app_state.pool.get().await?; - let uuid = check_access_token(auth.token(), &mut conn).await?; - let me = Me::get(&mut conn, uuid).await?; if me.email_verified { @@ -87,13 +79,9 @@ pub async fn get( /// pub async fn post( State(app_state): State>, - TypedHeader(auth): TypedHeader>, + Extension(CurrentUser(uuid)): Extension> ) -> Result { - let mut conn = app_state.pool.get().await?; - - let uuid = check_access_token(auth.token(), &mut conn).await?; - - let me = Me::get(&mut conn, uuid).await?; + let me = Me::get(&mut app_state.pool.get().await?, uuid).await?; if me.email_verified { return Ok(StatusCode::NO_CONTENT); diff --git a/src/api/v1/channels/mod.rs b/src/api/v1/channels/mod.rs index dc82b86..24b62f7 100644 --- a/src/api/v1/channels/mod.rs +++ b/src/api/v1/channels/mod.rs @@ -11,14 +11,9 @@ use crate::AppState; mod uuid; pub fn router() -> Router> { - //let (layer, io) = SocketIo::new_layer(); - - //io.ns("/{uuid}/socket", uuid::socket::ws); - Router::new() .route("/{uuid}", get(uuid::get)) .route("/{uuid}", delete(uuid::delete)) .route("/{uuid}", patch(uuid::patch)) .route("/{uuid}/messages", get(uuid::messages::get)) - //.layer(layer) } diff --git a/src/api/v1/channels/uuid/messages.rs b/src/api/v1/channels/uuid/messages.rs index 8c12ee0..0297bbc 100644 --- a/src/api/v1/channels/uuid/messages.rs +++ b/src/api/v1/channels/uuid/messages.rs @@ -3,22 +3,11 @@ use std::sync::Arc; use crate::{ - AppState, - api::v1::auth::check_access_token, - error::Error, - objects::{Channel, Member}, - utils::global_checks, + api::v1::auth::CurrentUser, error::Error, objects::{Channel, Member}, utils::global_checks, AppState }; use ::uuid::Uuid; use axum::{ - Json, - extract::{Path, Query, State}, - http::StatusCode, - response::IntoResponse, -}; -use axum_extra::{ - TypedHeader, - headers::{Authorization, authorization::Bearer}, + extract::{Path, Query, State}, http::StatusCode, response::IntoResponse, Extension, Json }; use serde::Deserialize; @@ -62,17 +51,13 @@ pub async fn get( State(app_state): State>, Path(channel_uuid): Path, Query(message_request): Query, - TypedHeader(auth): TypedHeader>, + Extension(CurrentUser(uuid)): Extension>, ) -> Result { - let mut conn = app_state.pool.get().await?; - - let uuid = check_access_token(auth.token(), &mut conn).await?; - global_checks(&app_state, uuid).await?; let channel = Channel::fetch_one(&app_state, channel_uuid).await?; - Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; + Member::check_membership(&mut app_state.pool.get().await?, uuid, channel.guild_uuid).await?; let messages = channel .fetch_messages(&app_state, message_request.amount, message_request.offset) diff --git a/src/api/v1/channels/uuid/mod.rs b/src/api/v1/channels/uuid/mod.rs index 3ce91c3..c1560f0 100644 --- a/src/api/v1/channels/uuid/mod.rs +++ b/src/api/v1/channels/uuid/mod.rs @@ -7,38 +7,28 @@ use std::sync::Arc; use crate::{ AppState, - api::v1::auth::check_access_token, + api::v1::auth::CurrentUser, error::Error, objects::{Channel, Member, Permissions}, utils::global_checks, }; use axum::{ - Json, - extract::{Path, State}, - http::StatusCode, - response::IntoResponse, -}; -use axum_extra::{ - TypedHeader, - headers::{Authorization, authorization::Bearer}, + extract::{Path, State}, http::StatusCode, response::IntoResponse, Extension, Json }; + use serde::Deserialize; use uuid::Uuid; pub async fn get( State(app_state): State>, Path(channel_uuid): Path, - TypedHeader(auth): TypedHeader>, + Extension(CurrentUser(uuid)): Extension>, ) -> Result { - let mut conn = app_state.pool.get().await?; - - let uuid = check_access_token(auth.token(), &mut conn).await?; - global_checks(&app_state, uuid).await?; let channel = Channel::fetch_one(&app_state, channel_uuid).await?; - Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; + Member::check_membership(&mut app_state.pool.get().await?, uuid, channel.guild_uuid).await?; Ok((StatusCode::OK, Json(channel))) } @@ -46,17 +36,13 @@ pub async fn get( pub async fn delete( State(app_state): State>, Path(channel_uuid): Path, - TypedHeader(auth): TypedHeader>, + Extension(CurrentUser(uuid)): Extension>, ) -> Result { - let mut conn = app_state.pool.get().await?; - - let uuid = check_access_token(auth.token(), &mut conn).await?; - global_checks(&app_state, uuid).await?; let channel = Channel::fetch_one(&app_state, channel_uuid).await?; - let member = Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; + let member = Member::check_membership(&mut app_state.pool.get().await?, uuid, channel.guild_uuid).await?; member .check_permission(&app_state, Permissions::ManageChannel) @@ -108,18 +94,14 @@ pub struct NewInfo { pub async fn patch( State(app_state): State>, Path(channel_uuid): Path, - TypedHeader(auth): TypedHeader>, + Extension(CurrentUser(uuid)): Extension>, Json(new_info): Json, ) -> Result { - let mut conn = app_state.pool.get().await?; - - let uuid = check_access_token(auth.token(), &mut conn).await?; - global_checks(&app_state, uuid).await?; let mut channel = Channel::fetch_one(&app_state, channel_uuid).await?; - let member = Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; + let member = Member::check_membership(&mut app_state.pool.get().await?, uuid, channel.guild_uuid).await?; member .check_permission(&app_state, Permissions::ManageChannel) diff --git a/src/api/v1/guilds/mod.rs b/src/api/v1/guilds/mod.rs index 18a117f..dbee589 100644 --- a/src/api/v1/guilds/mod.rs +++ b/src/api/v1/guilds/mod.rs @@ -3,26 +3,15 @@ use std::sync::Arc; use axum::{ - Json, Router, - extract::State, - http::StatusCode, - response::IntoResponse, - routing::{get, post}, -}; -use axum_extra::{ - TypedHeader, - headers::{Authorization, authorization::Bearer}, + extract::State, http::StatusCode, response::IntoResponse, routing::{get, post}, Extension, Json, Router }; use serde::Deserialize; +use ::uuid::Uuid; mod uuid; use crate::{ - AppState, - api::v1::auth::check_access_token, - error::Error, - objects::{Guild, StartAmountQuery}, - utils::global_checks, + api::v1::auth::CurrentUser, error::Error, objects::{Guild, StartAmountQuery}, utils::global_checks, AppState }; #[derive(Deserialize)] @@ -63,14 +52,10 @@ pub fn router() -> Router> { /// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps pub async fn new( State(app_state): State>, - TypedHeader(auth): TypedHeader>, + Extension(CurrentUser(uuid)): Extension>, Json(guild_info): Json, ) -> Result { - let mut conn = app_state.pool.get().await?; - - let uuid = check_access_token(auth.token(), &mut conn).await?; - - let guild = Guild::new(&mut conn, guild_info.name.clone(), uuid).await?; + let guild = Guild::new(&mut app_state.pool.get().await?, guild_info.name.clone(), uuid).await?; Ok((StatusCode::OK, Json(guild))) } @@ -124,15 +109,12 @@ pub async fn new( /// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps pub async fn get_guilds( State(app_state): State>, - TypedHeader(auth): TypedHeader>, + Extension(CurrentUser(uuid)): Extension>, Json(request_query): Json, ) -> Result { let start = request_query.start.unwrap_or(0); - let amount = request_query.amount.unwrap_or(10); - let uuid = check_access_token(auth.token(), &mut app_state.pool.get().await?).await?; - global_checks(&app_state, uuid).await?; let guilds = Guild::fetch_amount(&app_state.pool, start, amount).await?; diff --git a/src/api/v1/guilds/uuid/channels.rs b/src/api/v1/guilds/uuid/channels.rs index 0104566..a28aa6c 100644 --- a/src/api/v1/guilds/uuid/channels.rs +++ b/src/api/v1/guilds/uuid/channels.rs @@ -2,23 +2,12 @@ use std::sync::Arc; use ::uuid::Uuid; use axum::{ - Json, - extract::{Path, State}, - http::StatusCode, - response::IntoResponse, -}; -use axum_extra::{ - TypedHeader, - headers::{Authorization, authorization::Bearer}, + extract::{Path, State}, http::StatusCode, response::IntoResponse, Extension, Json }; use serde::Deserialize; use crate::{ - AppState, - api::v1::auth::check_access_token, - error::Error, - objects::{Channel, Member, Permissions}, - utils::{global_checks, order_by_is_above}, + api::v1::auth::CurrentUser, error::Error, objects::{Channel, Member, Permissions}, utils::{global_checks, order_by_is_above}, AppState }; #[derive(Deserialize)] @@ -30,15 +19,11 @@ pub struct ChannelInfo { pub async fn get( State(app_state): State>, Path(guild_uuid): Path, - TypedHeader(auth): TypedHeader>, + Extension(CurrentUser(uuid)): Extension>, ) -> Result { - let mut conn = app_state.pool.get().await?; - - let uuid = check_access_token(auth.token(), &mut conn).await?; - global_checks(&app_state, uuid).await?; - Member::check_membership(&mut conn, uuid, guild_uuid).await?; + Member::check_membership(&mut app_state.pool.get().await?, uuid, guild_uuid).await?; if let Ok(cache_hit) = app_state .get_cache_key(format!("{guild_uuid}_channels")) @@ -65,16 +50,12 @@ pub async fn get( pub async fn create( State(app_state): State>, Path(guild_uuid): Path, - TypedHeader(auth): TypedHeader>, + Extension(CurrentUser(uuid)): Extension>, Json(channel_info): Json, ) -> Result { - let mut conn = app_state.pool.get().await?; - - let uuid = check_access_token(auth.token(), &mut conn).await?; - global_checks(&app_state, uuid).await?; - let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; + let member = Member::check_membership(&mut app_state.pool.get().await?, uuid, guild_uuid).await?; member .check_permission(&app_state, Permissions::ManageChannel) diff --git a/src/api/v1/guilds/uuid/invites/mod.rs b/src/api/v1/guilds/uuid/invites/mod.rs index 7703cf7..2070452 100644 --- a/src/api/v1/guilds/uuid/invites/mod.rs +++ b/src/api/v1/guilds/uuid/invites/mod.rs @@ -1,21 +1,14 @@ use std::sync::Arc; use axum::{ - Json, - extract::{Path, State}, - http::StatusCode, - response::IntoResponse, -}; -use axum_extra::{ - TypedHeader, - headers::{Authorization, authorization::Bearer}, + extract::{Path, State}, http::StatusCode, response::IntoResponse, Extension, Json }; use serde::Deserialize; use uuid::Uuid; use crate::{ AppState, - api::v1::auth::check_access_token, + api::v1::auth::CurrentUser, error::Error, objects::{Guild, Member, Permissions}, utils::global_checks, @@ -29,14 +22,12 @@ pub struct InviteRequest { pub async fn get( State(app_state): State>, Path(guild_uuid): Path, - TypedHeader(auth): TypedHeader>, + Extension(CurrentUser(uuid)): Extension>, ) -> Result { - let mut conn = app_state.pool.get().await?; - - let uuid = check_access_token(auth.token(), &mut conn).await?; - global_checks(&app_state, uuid).await?; + let mut conn = app_state.pool.get().await?; + Member::check_membership(&mut conn, uuid, guild_uuid).await?; let guild = Guild::fetch_one(&mut conn, guild_uuid).await?; @@ -49,15 +40,13 @@ pub async fn get( pub async fn create( State(app_state): State>, Path(guild_uuid): Path, - TypedHeader(auth): TypedHeader>, + Extension(CurrentUser(uuid)): Extension>, Json(invite_request): Json, ) -> Result { - let mut conn = app_state.pool.get().await?; - - let uuid = check_access_token(auth.token(), &mut conn).await?; - global_checks(&app_state, uuid).await?; + let mut conn = app_state.pool.get().await?; + let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; member diff --git a/src/api/v1/guilds/uuid/members.rs b/src/api/v1/guilds/uuid/members.rs index bd2f853..6c8b980 100644 --- a/src/api/v1/guilds/uuid/members.rs +++ b/src/api/v1/guilds/uuid/members.rs @@ -2,19 +2,12 @@ use std::sync::Arc; use ::uuid::Uuid; use axum::{ - Json, - extract::{Path, State}, - http::StatusCode, - response::IntoResponse, -}; -use axum_extra::{ - TypedHeader, - headers::{Authorization, authorization::Bearer}, + extract::{Path, State}, http::StatusCode, response::IntoResponse, Extension, Json }; use crate::{ AppState, - api::v1::auth::check_access_token, + api::v1::auth::CurrentUser, error::Error, objects::{Me, Member}, utils::global_checks, @@ -23,14 +16,12 @@ use crate::{ pub async fn get( State(app_state): State>, Path(guild_uuid): Path, - TypedHeader(auth): TypedHeader>, + Extension(CurrentUser(uuid)): Extension>, ) -> Result { - let mut conn = app_state.pool.get().await?; - - let uuid = check_access_token(auth.token(), &mut conn).await?; - global_checks(&app_state, uuid).await?; + let mut conn = app_state.pool.get().await?; + Member::check_membership(&mut conn, uuid, guild_uuid).await?; let me = Me::get(&mut conn, uuid).await?; diff --git a/src/api/v1/guilds/uuid/mod.rs b/src/api/v1/guilds/uuid/mod.rs index 0a27123..c5a809f 100644 --- a/src/api/v1/guilds/uuid/mod.rs +++ b/src/api/v1/guilds/uuid/mod.rs @@ -3,15 +3,7 @@ use std::sync::Arc; use axum::{ - Json, Router, - extract::{Multipart, Path, State}, - http::StatusCode, - response::IntoResponse, - routing::{get, patch, post}, -}; -use axum_extra::{ - TypedHeader, - headers::{Authorization, authorization::Bearer}, + extract::{Multipart, Path, State}, http::StatusCode, response::IntoResponse, routing::{get, patch, post}, Extension, Json, Router }; use bytes::Bytes; use uuid::Uuid; @@ -23,7 +15,7 @@ mod roles; use crate::{ AppState, - api::v1::auth::check_access_token, + api::v1::auth::CurrentUser, error::Error, objects::{Guild, Member, Permissions}, utils::global_checks, @@ -84,14 +76,12 @@ pub fn router() -> Router> { pub async fn get_guild( State(app_state): State>, Path(guild_uuid): Path, - TypedHeader(auth): TypedHeader>, + Extension(CurrentUser(uuid)): Extension>, ) -> Result { - let mut conn = app_state.pool.get().await?; - - let uuid = check_access_token(auth.token(), &mut conn).await?; - global_checks(&app_state, uuid).await?; + let mut conn = app_state.pool.get().await?; + Member::check_membership(&mut conn, uuid, guild_uuid).await?; let guild = Guild::fetch_one(&mut conn, guild_uuid).await?; @@ -105,15 +95,13 @@ pub async fn get_guild( pub async fn edit( State(app_state): State>, Path(guild_uuid): Path, - TypedHeader(auth): TypedHeader>, + Extension(CurrentUser(uuid)): Extension>, mut multipart: Multipart, ) -> Result { - let mut conn = app_state.pool.get().await?; - - let uuid = check_access_token(auth.token(), &mut conn).await?; - global_checks(&app_state, uuid).await?; + let mut conn = app_state.pool.get().await?; + let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; member diff --git a/src/api/v1/guilds/uuid/roles/mod.rs b/src/api/v1/guilds/uuid/roles/mod.rs index 12960c2..5331143 100644 --- a/src/api/v1/guilds/uuid/roles/mod.rs +++ b/src/api/v1/guilds/uuid/roles/mod.rs @@ -2,20 +2,13 @@ use std::sync::Arc; use ::uuid::Uuid; use axum::{ - Json, - extract::{Path, State}, - http::StatusCode, - response::IntoResponse, -}; -use axum_extra::{ - TypedHeader, - headers::{Authorization, authorization::Bearer}, + extract::{Path, State}, http::StatusCode, response::IntoResponse, Extension, Json }; use serde::Deserialize; use crate::{ AppState, - api::v1::auth::check_access_token, + api::v1::auth::CurrentUser, error::Error, objects::{Member, Permissions, Role}, utils::{global_checks, order_by_is_above}, @@ -31,11 +24,11 @@ pub struct RoleInfo { pub async fn get( State(app_state): State>, Path(guild_uuid): Path, - TypedHeader(auth): TypedHeader>, + Extension(CurrentUser(uuid)): Extension>, ) -> Result { - let mut conn = app_state.pool.get().await?; + global_checks(&app_state, uuid).await?; - let uuid = check_access_token(auth.token(), &mut conn).await?; + let mut conn = app_state.pool.get().await?; Member::check_membership(&mut conn, uuid, guild_uuid).await?; @@ -57,15 +50,13 @@ pub async fn get( pub async fn create( State(app_state): State>, Path(guild_uuid): Path, - TypedHeader(auth): TypedHeader>, + Extension(CurrentUser(uuid)): Extension>, Json(role_info): Json, ) -> Result { - let mut conn = app_state.pool.get().await?; - - let uuid = check_access_token(auth.token(), &mut conn).await?; - global_checks(&app_state, uuid).await?; + let mut conn = app_state.pool.get().await?; + let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; member diff --git a/src/api/v1/guilds/uuid/roles/uuid.rs b/src/api/v1/guilds/uuid/roles/uuid.rs index a62a5b4..91300bf 100644 --- a/src/api/v1/guilds/uuid/roles/uuid.rs +++ b/src/api/v1/guilds/uuid/roles/uuid.rs @@ -2,19 +2,12 @@ use std::sync::Arc; use ::uuid::Uuid; use axum::{ - Json, - extract::{Path, State}, - http::StatusCode, - response::IntoResponse, -}; -use axum_extra::{ - TypedHeader, - headers::{Authorization, authorization::Bearer}, + extract::{Path, State}, http::StatusCode, response::IntoResponse, Extension, Json }; use crate::{ AppState, - api::v1::auth::check_access_token, + api::v1::auth::CurrentUser, error::Error, objects::{Member, Role}, utils::global_checks, @@ -23,14 +16,12 @@ use crate::{ pub async fn get( State(app_state): State>, Path((guild_uuid, role_uuid)): Path<(Uuid, Uuid)>, - TypedHeader(auth): TypedHeader>, + Extension(CurrentUser(uuid)): Extension>, ) -> Result { - let mut conn = app_state.pool.get().await?; - - let uuid = check_access_token(auth.token(), &mut conn).await?; - global_checks(&app_state, uuid).await?; + let mut conn = app_state.pool.get().await?; + Member::check_membership(&mut conn, uuid, guild_uuid).await?; if let Ok(cache_hit) = app_state.get_cache_key(format!("{role_uuid}")).await { diff --git a/src/api/v1/invites/id.rs b/src/api/v1/invites/id.rs index b832557..c752177 100644 --- a/src/api/v1/invites/id.rs +++ b/src/api/v1/invites/id.rs @@ -1,19 +1,13 @@ use std::sync::Arc; use axum::{ - Json, - extract::{Path, State}, - http::StatusCode, - response::IntoResponse, -}; -use axum_extra::{ - TypedHeader, - headers::{Authorization, authorization::Bearer}, + extract::{Path, State}, http::StatusCode, response::IntoResponse, Extension, Json }; +use uuid::Uuid; use crate::{ AppState, - api::v1::auth::check_access_token, + api::v1::auth::CurrentUser, error::Error, objects::{Guild, Invite, Member}, utils::global_checks, @@ -35,14 +29,12 @@ pub async fn get( pub async fn join( State(app_state): State>, Path(invite_id): Path, - TypedHeader(auth): TypedHeader>, + Extension(CurrentUser(uuid)): Extension>, ) -> Result { - let mut conn = app_state.pool.get().await?; - - let uuid = check_access_token(auth.token(), &mut conn).await?; - global_checks(&app_state, uuid).await?; + let mut conn = app_state.pool.get().await?; + let invite = Invite::fetch_one(&mut conn, invite_id).await?; let guild = Guild::fetch_one(&mut conn, invite.guild_uuid).await?; diff --git a/src/api/v1/me/friends/mod.rs b/src/api/v1/me/friends/mod.rs index 8a7851c..63284a8 100644 --- a/src/api/v1/me/friends/mod.rs +++ b/src/api/v1/me/friends/mod.rs @@ -1,34 +1,23 @@ use std::sync::Arc; -use axum::{Json, extract::State, http::StatusCode, response::IntoResponse}; -use axum_extra::{ - TypedHeader, - headers::{Authorization, authorization::Bearer}, -}; +use axum::{extract::State, http::StatusCode, response::IntoResponse, Extension, Json}; use serde::Deserialize; +use ::uuid::Uuid; pub mod uuid; use crate::{ - AppState, - api::v1::auth::check_access_token, - error::Error, - objects::Me, - utils::{global_checks, user_uuid_from_username}, + api::v1::auth::CurrentUser, error::Error, objects::Me, utils::{global_checks, user_uuid_from_username}, AppState }; /// Returns a list of users that are your friends pub async fn get( State(app_state): State>, - TypedHeader(auth): TypedHeader>, + Extension(CurrentUser(uuid)): Extension>, ) -> Result { - let mut conn = app_state.pool.get().await?; - - let uuid = check_access_token(auth.token(), &mut conn).await?; - global_checks(&app_state, uuid).await?; - let me = Me::get(&mut conn, uuid).await?; + let me = Me::get(&mut app_state.pool.get().await?, uuid).await?; let friends = me.get_friends(&app_state).await?; @@ -61,15 +50,13 @@ pub struct UserReq { /// pub async fn post( State(app_state): State>, - TypedHeader(auth): TypedHeader>, + Extension(CurrentUser(uuid)): Extension>, Json(user_request): Json, ) -> Result { - let mut conn = app_state.pool.get().await?; - - let uuid = check_access_token(auth.token(), &mut conn).await?; - global_checks(&app_state, uuid).await?; + let mut conn = app_state.pool.get().await?; + let me = Me::get(&mut conn, uuid).await?; let target_uuid = user_uuid_from_username(&mut conn, &user_request.username).await?; diff --git a/src/api/v1/me/friends/uuid.rs b/src/api/v1/me/friends/uuid.rs index 8d40f26..5a32386 100644 --- a/src/api/v1/me/friends/uuid.rs +++ b/src/api/v1/me/friends/uuid.rs @@ -3,29 +3,23 @@ use std::sync::Arc; use axum::{ extract::{Path, State}, http::StatusCode, - response::IntoResponse, -}; -use axum_extra::{ - TypedHeader, - headers::{Authorization, authorization::Bearer}, + response::IntoResponse, Extension, }; use uuid::Uuid; use crate::{ - AppState, api::v1::auth::check_access_token, error::Error, objects::Me, utils::global_checks, + AppState, api::v1::auth::CurrentUser, error::Error, objects::Me, utils::global_checks, }; pub async fn delete( State(app_state): State>, Path(friend_uuid): Path, - TypedHeader(auth): TypedHeader>, + Extension(CurrentUser(uuid)): Extension>, ) -> Result { - let mut conn = app_state.pool.get().await?; - - let uuid = check_access_token(auth.token(), &mut conn).await?; - global_checks(&app_state, uuid).await?; + let mut conn = app_state.pool.get().await?; + let me = Me::get(&mut conn, uuid).await?; me.remove_friend(&mut conn, friend_uuid).await?; diff --git a/src/api/v1/me/guilds.rs b/src/api/v1/me/guilds.rs index adfe845..a2d2111 100644 --- a/src/api/v1/me/guilds.rs +++ b/src/api/v1/me/guilds.rs @@ -2,14 +2,11 @@ use std::sync::Arc; -use axum::{Json, extract::State, http::StatusCode, response::IntoResponse}; -use axum_extra::{ - TypedHeader, - headers::{Authorization, authorization::Bearer}, -}; +use axum::{extract::State, http::StatusCode, response::IntoResponse, Extension, Json}; +use uuid::Uuid; use crate::{ - AppState, api::v1::auth::check_access_token, error::Error, objects::Me, utils::global_checks, + AppState, api::v1::auth::CurrentUser, error::Error, objects::Me, utils::global_checks, }; /// `GET /api/v1/me/guilds` Returns all guild memberships in a list @@ -59,14 +56,12 @@ use crate::{ /// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps pub async fn get( State(app_state): State>, - TypedHeader(auth): TypedHeader>, + Extension(CurrentUser(uuid)): Extension>, ) -> Result { - let mut conn = app_state.pool.get().await?; - - let uuid = check_access_token(auth.token(), &mut conn).await?; - global_checks(&app_state, uuid).await?; + let mut conn = app_state.pool.get().await?; + let me = Me::get(&mut conn, uuid).await?; let memberships = me.fetch_memberships(&mut conn).await?; diff --git a/src/api/v1/me/mod.rs b/src/api/v1/me/mod.rs index e9680bc..ce577d4 100644 --- a/src/api/v1/me/mod.rs +++ b/src/api/v1/me/mod.rs @@ -1,21 +1,14 @@ use std::sync::Arc; use axum::{ - Json, Router, - extract::{DefaultBodyLimit, Multipart, State}, - http::StatusCode, - response::IntoResponse, - routing::{delete, get, patch, post}, -}; -use axum_extra::{ - TypedHeader, - headers::{Authorization, authorization::Bearer}, + extract::{DefaultBodyLimit, Multipart, State}, http::StatusCode, response::IntoResponse, routing::{delete, get, patch, post}, Extension, Json, Router }; use bytes::Bytes; use serde::Deserialize; +use uuid::Uuid; use crate::{ - AppState, api::v1::auth::check_access_token, error::Error, objects::Me, utils::global_checks, + api::v1::auth::CurrentUser, error::Error, objects::Me, utils::global_checks, AppState }; mod friends; @@ -38,13 +31,9 @@ pub fn router() -> Router> { pub async fn get_me( State(app_state): State>, - TypedHeader(auth): TypedHeader>, + Extension(CurrentUser(uuid)): Extension>, ) -> Result { - let mut conn = app_state.pool.get().await?; - - let uuid = check_access_token(auth.token(), &mut conn).await?; - - let me = Me::get(&mut conn, uuid).await?; + let me = Me::get(&mut app_state.pool.get().await?, uuid).await?; Ok((StatusCode::OK, Json(me))) } @@ -60,13 +49,9 @@ struct NewInfo { pub async fn update( State(app_state): State>, - TypedHeader(auth): TypedHeader>, + Extension(CurrentUser(uuid)): Extension>, mut multipart: Multipart, ) -> Result { - let mut conn = app_state.pool.get().await?; - - let uuid = check_access_token(auth.token(), &mut conn).await?; - let mut json_raw: Option = None; let mut avatar: Option = None; @@ -88,7 +73,7 @@ pub async fn update( global_checks(&app_state, uuid).await?; } - let mut me = Me::get(&mut conn, uuid).await?; + let mut me = Me::get(&mut app_state.pool.get().await?, uuid).await?; if let Some(avatar) = avatar { me.set_avatar(&app_state, app_state.config.bunny.cdn_url.clone(), avatar) diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs index 4e8654b..f3e4305 100644 --- a/src/api/v1/mod.rs +++ b/src/api/v1/mod.rs @@ -2,9 +2,9 @@ use std::sync::Arc; -use axum::{routing::get, Router}; +use axum::{middleware::from_fn_with_state, routing::get, Router}; -use crate::AppState; +use crate::{api::v1::auth::CurrentUser, AppState}; mod auth; mod channels; @@ -14,13 +14,17 @@ mod me; mod stats; mod users; -pub fn router() -> Router> { - Router::new() - .route("/stats", get(stats::res)) - .nest("/auth", auth::router()) +pub fn router(app_state: Arc) -> Router> { + let router_with_auth = Router::new() .nest("/users", users::router()) .nest("/channels", channels::router()) .nest("/guilds", guilds::router()) .nest("/invites", invites::router()) .nest("/me", me::router()) + .layer(from_fn_with_state(app_state.clone(), CurrentUser::check_auth_layer)); + + Router::new() + .route("/stats", get(stats::res)) + .nest("/auth", auth::router(app_state)) + .merge(router_with_auth) } diff --git a/src/api/v1/users/mod.rs b/src/api/v1/users/mod.rs index f0d09c5..82f2125 100644 --- a/src/api/v1/users/mod.rs +++ b/src/api/v1/users/mod.rs @@ -3,23 +3,12 @@ use std::sync::Arc; use axum::{ - Json, Router, - extract::{Query, State}, - http::StatusCode, - response::IntoResponse, - routing::get, -}; -use axum_extra::{ - TypedHeader, - headers::{Authorization, authorization::Bearer}, + extract::{Query, State}, http::StatusCode, response::IntoResponse, routing::get, Extension, Json, Router }; +use ::uuid::Uuid; use crate::{ - AppState, - api::v1::auth::check_access_token, - error::Error, - objects::{StartAmountQuery, User}, - utils::global_checks, + api::v1::auth::CurrentUser, error::Error, objects::{StartAmountQuery, User}, utils::global_checks, AppState }; mod uuid; @@ -63,7 +52,7 @@ pub fn router() -> Router> { pub async fn users( State(app_state): State>, Query(request_query): Query, - TypedHeader(auth): TypedHeader>, + Extension(CurrentUser(uuid)): Extension>, ) -> Result { let start = request_query.start.unwrap_or(0); @@ -73,13 +62,9 @@ pub async fn users( return Ok(StatusCode::BAD_REQUEST.into_response()); } - let mut conn = app_state.pool.get().await?; - - let uuid = check_access_token(auth.token(), &mut conn).await?; - global_checks(&app_state, uuid).await?; - let users = User::fetch_amount(&mut conn, start, amount).await?; + let users = User::fetch_amount(&mut app_state.pool.get().await?, start, amount).await?; Ok((StatusCode::OK, Json(users)).into_response()) } diff --git a/src/api/v1/users/uuid.rs b/src/api/v1/users/uuid.rs index 1b7d43b..2bdcfac 100644 --- a/src/api/v1/users/uuid.rs +++ b/src/api/v1/users/uuid.rs @@ -3,23 +3,12 @@ use std::sync::Arc; use axum::{ - Json, - extract::{Path, State}, - http::StatusCode, - response::IntoResponse, -}; -use axum_extra::{ - TypedHeader, - headers::{Authorization, authorization::Bearer}, + extract::{Path, State}, http::StatusCode, response::IntoResponse, Extension, Json }; use uuid::Uuid; use crate::{ - AppState, - api::v1::auth::check_access_token, - error::Error, - objects::{Me, User}, - utils::global_checks, + api::v1::auth::CurrentUser, error::Error, objects::{Me, User}, utils::global_checks, AppState }; /// `GET /api/v1/users/{uuid}` Returns user with the given UUID @@ -41,15 +30,11 @@ use crate::{ pub async fn get( State(app_state): State>, Path(user_uuid): Path, - TypedHeader(auth): TypedHeader>, + Extension(CurrentUser(uuid)): Extension>, ) -> Result { - let mut conn = app_state.pool.get().await?; - - let uuid = check_access_token(auth.token(), &mut conn).await?; - global_checks(&app_state, uuid).await?; - let me = Me::get(&mut conn, uuid).await?; + let me = Me::get(&mut app_state.pool.get().await?, uuid).await?; let user = User::fetch_one_with_friendship(&app_state, &me, user_uuid).await?; diff --git a/src/main.rs b/src/main.rs index ab37924..8e6effc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -163,7 +163,7 @@ async fn main() -> Result<(), Error> { // build our application with a route let app = Router::new() // `GET /` goes to `root` - .merge(api::router(web.backend_url.path().trim_end_matches("/"))) + .merge(api::router(web.backend_url.path().trim_end_matches("/"), app_state.clone())) .with_state(app_state) .layer(cors) .layer(socket_io); From a602c2624f27f846a527b453f2caa0f822af29d2 Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 20 Jul 2025 16:30:46 +0200 Subject: [PATCH 106/160] style: cargo fmt & clippy fixes --- src/api/mod.rs | 2 +- src/api/v1/auth/devices.rs | 7 +++++-- src/api/v1/auth/login.rs | 13 ++++++++++--- src/api/v1/auth/mod.rs | 18 +++++++++++------ src/api/v1/auth/refresh.rs | 16 +++++++++++---- src/api/v1/auth/register.rs | 21 +++++++++++++------- src/api/v1/auth/revoke.rs | 10 ++++++++-- src/api/v1/auth/verify_email.rs | 12 ++++++++---- src/api/v1/channels/uuid/messages.rs | 11 +++++++++-- src/api/v1/channels/uuid/mod.rs | 13 ++++++++++--- src/api/v1/guilds/mod.rs | 21 ++++++++++++++++---- src/api/v1/guilds/uuid/channels.rs | 14 +++++++++++--- src/api/v1/guilds/uuid/invites/mod.rs | 5 ++++- src/api/v1/guilds/uuid/members.rs | 5 ++++- src/api/v1/guilds/uuid/mod.rs | 6 +++++- src/api/v1/guilds/uuid/roles/mod.rs | 5 ++++- src/api/v1/guilds/uuid/roles/uuid.rs | 5 ++++- src/api/v1/invites/id.rs | 5 ++++- src/api/v1/me/friends/mod.rs | 10 +++++++--- src/api/v1/me/friends/uuid.rs | 3 ++- src/api/v1/me/guilds.rs | 2 +- src/api/v1/me/mod.rs | 8 ++++++-- src/api/v1/mod.rs | 9 ++++++--- src/api/v1/users/mod.rs | 16 +++++++++++---- src/api/v1/users/uuid.rs | 11 +++++++++-- src/main.rs | 28 ++++++++++++++++----------- src/socket.rs | 14 ++++++++------ src/utils.rs | 6 +++--- src/wordlist.rs | 6 +++--- 29 files changed, 216 insertions(+), 86 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index 988ee45..5aaa8a5 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -2,7 +2,7 @@ use std::sync::Arc; -use axum::{routing::get, Router}; +use axum::{Router, routing::get}; use crate::AppState; diff --git a/src/api/v1/auth/devices.rs b/src/api/v1/auth/devices.rs index 336a52f..35fe957 100644 --- a/src/api/v1/auth/devices.rs +++ b/src/api/v1/auth/devices.rs @@ -2,14 +2,17 @@ use std::sync::Arc; -use axum::{extract::State, http::StatusCode, response::IntoResponse, Extension, Json}; +use axum::{Extension, Json, extract::State, http::StatusCode, response::IntoResponse}; use diesel::{ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper}; use diesel_async::RunQueryDsl; use serde::Serialize; use uuid::Uuid; use crate::{ - api::v1::auth::CurrentUser, error::Error, schema::refresh_tokens::{self, dsl}, AppState + AppState, + api::v1::auth::CurrentUser, + error::Error, + schema::refresh_tokens::{self, dsl}, }; #[derive(Serialize, Selectable, Queryable)] diff --git a/src/api/v1/auth/login.rs b/src/api/v1/auth/login.rs index 61cb6a0..22cc838 100644 --- a/src/api/v1/auth/login.rs +++ b/src/api/v1/auth/login.rs @@ -20,8 +20,8 @@ use crate::{ error::Error, schema::*, utils::{ - PASSWORD_REGEX, generate_token, new_refresh_token_cookie, - user_uuid_from_identifier, generate_device_name + PASSWORD_REGEX, generate_device_name, generate_token, new_refresh_token_cookie, + user_uuid_from_identifier, }, }; @@ -95,7 +95,14 @@ pub async fn response( .execute(&mut conn) .await?; - let mut response = (StatusCode::OK, Json(Response { access_token, device_name })).into_response(); + let mut response = ( + StatusCode::OK, + Json(Response { + access_token, + device_name, + }), + ) + .into_response(); response.headers_mut().append( "Set-Cookie", diff --git a/src/api/v1/auth/mod.rs b/src/api/v1/auth/mod.rs index c579899..9a72f11 100644 --- a/src/api/v1/auth/mod.rs +++ b/src/api/v1/auth/mod.rs @@ -4,9 +4,16 @@ use std::{ }; use axum::{ - extract::{Request, State}, middleware::{from_fn_with_state, Next}, response::IntoResponse, routing::{delete, get, post}, Router + Router, + extract::{Request, State}, + middleware::{Next, from_fn_with_state}, + response::IntoResponse, + routing::{delete, get, post}, +}; +use axum_extra::{ + TypedHeader, + headers::{Authorization, authorization::Bearer}, }; -use axum_extra::{headers::{authorization::Bearer, Authorization}, TypedHeader}; use diesel::{ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; use serde::Serialize; @@ -23,14 +30,12 @@ mod reset_password; mod revoke; mod verify_email; - #[derive(Serialize)] pub struct Response { access_token: String, device_name: String, } - pub fn router(app_state: Arc) -> Router> { let router_with_auth = Router::new() .route("/verify-email", get(verify_email::get)) @@ -82,9 +87,10 @@ impl CurrentUser { State(app_state): State>, TypedHeader(auth): TypedHeader>, mut req: Request, - next: Next + next: Next, ) -> Result { - let current_user = CurrentUser(check_access_token(auth.token(), &mut app_state.pool.get().await?).await?); + let current_user = + CurrentUser(check_access_token(auth.token(), &mut app_state.pool.get().await?).await?); req.extensions_mut().insert(current_user); Ok(next.run(req).await) diff --git a/src/api/v1/auth/refresh.rs b/src/api/v1/auth/refresh.rs index ee4f7ae..4b96226 100644 --- a/src/api/v1/auth/refresh.rs +++ b/src/api/v1/auth/refresh.rs @@ -1,7 +1,8 @@ use axum::{ + Json, extract::State, http::{HeaderValue, StatusCode}, - response::IntoResponse, Json, + response::IntoResponse, }; use axum_extra::extract::CookieJar; use diesel::{ExpressionMethods, QueryDsl, delete, update}; @@ -19,7 +20,8 @@ use crate::{ schema::{ access_tokens::{self, dsl}, refresh_tokens::{self, dsl as rdsl}, - }, utils::{generate_token, new_refresh_token_cookie} + }, + utils::{generate_token, new_refresh_token_cookie}, }; pub async fn post( @@ -104,7 +106,14 @@ pub async fn post( .execute(&mut conn) .await?; - let mut response = (StatusCode::OK, Json(Response { access_token, device_name })).into_response(); + let mut response = ( + StatusCode::OK, + Json(Response { + access_token, + device_name, + }), + ) + .into_response(); // TODO: Dont set this when refresh token is unchanged response.headers_mut().append( @@ -113,7 +122,6 @@ pub async fn post( &new_refresh_token_cookie(&app_state.config, refresh_token).to_string(), )?, ); - return Ok(response); } diff --git a/src/api/v1/auth/register.rs b/src/api/v1/auth/register.rs index f2520bf..807fab8 100644 --- a/src/api/v1/auth/register.rs +++ b/src/api/v1/auth/register.rs @@ -29,8 +29,8 @@ use crate::{ users::{self, dsl as udsl}, }, utils::{ - EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX, generate_token, - new_refresh_token_cookie, generate_device_name + EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX, generate_device_name, generate_token, + new_refresh_token_cookie, }, }; @@ -137,11 +137,11 @@ pub async fn post( let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; let device_name = generate_device_name(); - + insert_into(refresh_tokens::table) - .values(( - rdsl::token.eq(&refresh_token), - rdsl::uuid.eq(uuid), + .values(( + rdsl::token.eq(&refresh_token), + rdsl::uuid.eq(uuid), rdsl::created_at.eq(current_time), rdsl::device_name.eq(&device_name), )) @@ -162,7 +162,14 @@ pub async fn post( Member::new(&app_state, uuid, initial_guild).await?; } - let mut response = (StatusCode::OK, Json(Response {access_token, device_name})).into_response(); + let mut response = ( + StatusCode::OK, + Json(Response { + access_token, + device_name, + }), + ) + .into_response(); response.headers_mut().append( "Set-Cookie", diff --git a/src/api/v1/auth/revoke.rs b/src/api/v1/auth/revoke.rs index b59172e..dd87ec3 100644 --- a/src/api/v1/auth/revoke.rs +++ b/src/api/v1/auth/revoke.rs @@ -1,14 +1,20 @@ use std::sync::Arc; use argon2::{PasswordHash, PasswordVerifier}; -use axum::{extract::State, http::StatusCode, response::IntoResponse, Extension, Json}; +use axum::{Extension, Json, extract::State, http::StatusCode, response::IntoResponse}; use diesel::{ExpressionMethods, QueryDsl, delete}; use diesel_async::RunQueryDsl; use serde::Deserialize; use uuid::Uuid; use crate::{ - api::v1::auth::CurrentUser, error::Error, schema::{refresh_tokens::{self, dsl as rdsl}, users::dsl as udsl}, AppState + AppState, + api::v1::auth::CurrentUser, + error::Error, + schema::{ + refresh_tokens::{self, dsl as rdsl}, + users::dsl as udsl, + }, }; #[derive(Deserialize)] diff --git a/src/api/v1/auth/verify_email.rs b/src/api/v1/auth/verify_email.rs index 1270966..0801768 100644 --- a/src/api/v1/auth/verify_email.rs +++ b/src/api/v1/auth/verify_email.rs @@ -3,16 +3,20 @@ use std::sync::Arc; use axum::{ + Extension, extract::{Query, State}, http::StatusCode, - response::IntoResponse, Extension, + response::IntoResponse, }; use chrono::{Duration, Utc}; use serde::Deserialize; use uuid::Uuid; use crate::{ - api::v1::auth::CurrentUser, error::Error, objects::{EmailToken, Me}, AppState + AppState, + api::v1::auth::CurrentUser, + error::Error, + objects::{EmailToken, Me}, }; #[derive(Deserialize)] @@ -41,7 +45,7 @@ pub struct QueryParams { pub async fn get( State(app_state): State>, Query(query): Query, - Extension(CurrentUser(uuid)): Extension> + Extension(CurrentUser(uuid)): Extension>, ) -> Result { let mut conn = app_state.pool.get().await?; @@ -79,7 +83,7 @@ pub async fn get( /// pub async fn post( State(app_state): State>, - Extension(CurrentUser(uuid)): Extension> + Extension(CurrentUser(uuid)): Extension>, ) -> Result { let me = Me::get(&mut app_state.pool.get().await?, uuid).await?; diff --git a/src/api/v1/channels/uuid/messages.rs b/src/api/v1/channels/uuid/messages.rs index 0297bbc..b8f0ad6 100644 --- a/src/api/v1/channels/uuid/messages.rs +++ b/src/api/v1/channels/uuid/messages.rs @@ -3,11 +3,18 @@ use std::sync::Arc; use crate::{ - api::v1::auth::CurrentUser, error::Error, objects::{Channel, Member}, utils::global_checks, AppState + AppState, + api::v1::auth::CurrentUser, + error::Error, + objects::{Channel, Member}, + utils::global_checks, }; use ::uuid::Uuid; use axum::{ - extract::{Path, Query, State}, http::StatusCode, response::IntoResponse, Extension, Json + Extension, Json, + extract::{Path, Query, State}, + http::StatusCode, + response::IntoResponse, }; use serde::Deserialize; diff --git a/src/api/v1/channels/uuid/mod.rs b/src/api/v1/channels/uuid/mod.rs index c1560f0..5c88a29 100644 --- a/src/api/v1/channels/uuid/mod.rs +++ b/src/api/v1/channels/uuid/mod.rs @@ -13,7 +13,10 @@ use crate::{ utils::global_checks, }; use axum::{ - extract::{Path, State}, http::StatusCode, response::IntoResponse, Extension, Json + Extension, Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, }; use serde::Deserialize; @@ -42,7 +45,9 @@ pub async fn delete( let channel = Channel::fetch_one(&app_state, channel_uuid).await?; - let member = Member::check_membership(&mut app_state.pool.get().await?, uuid, channel.guild_uuid).await?; + let member = + Member::check_membership(&mut app_state.pool.get().await?, uuid, channel.guild_uuid) + .await?; member .check_permission(&app_state, Permissions::ManageChannel) @@ -101,7 +106,9 @@ pub async fn patch( let mut channel = Channel::fetch_one(&app_state, channel_uuid).await?; - let member = Member::check_membership(&mut app_state.pool.get().await?, uuid, channel.guild_uuid).await?; + let member = + Member::check_membership(&mut app_state.pool.get().await?, uuid, channel.guild_uuid) + .await?; member .check_permission(&app_state, Permissions::ManageChannel) diff --git a/src/api/v1/guilds/mod.rs b/src/api/v1/guilds/mod.rs index dbee589..8118522 100644 --- a/src/api/v1/guilds/mod.rs +++ b/src/api/v1/guilds/mod.rs @@ -2,16 +2,24 @@ use std::sync::Arc; +use ::uuid::Uuid; use axum::{ - extract::State, http::StatusCode, response::IntoResponse, routing::{get, post}, Extension, Json, Router + Extension, Json, Router, + extract::State, + http::StatusCode, + response::IntoResponse, + routing::{get, post}, }; use serde::Deserialize; -use ::uuid::Uuid; mod uuid; use crate::{ - api::v1::auth::CurrentUser, error::Error, objects::{Guild, StartAmountQuery}, utils::global_checks, AppState + AppState, + api::v1::auth::CurrentUser, + error::Error, + objects::{Guild, StartAmountQuery}, + utils::global_checks, }; #[derive(Deserialize)] @@ -55,7 +63,12 @@ pub async fn new( Extension(CurrentUser(uuid)): Extension>, Json(guild_info): Json, ) -> Result { - let guild = Guild::new(&mut app_state.pool.get().await?, guild_info.name.clone(), uuid).await?; + let guild = Guild::new( + &mut app_state.pool.get().await?, + guild_info.name.clone(), + uuid, + ) + .await?; Ok((StatusCode::OK, Json(guild))) } diff --git a/src/api/v1/guilds/uuid/channels.rs b/src/api/v1/guilds/uuid/channels.rs index a28aa6c..836982d 100644 --- a/src/api/v1/guilds/uuid/channels.rs +++ b/src/api/v1/guilds/uuid/channels.rs @@ -2,12 +2,19 @@ use std::sync::Arc; use ::uuid::Uuid; use axum::{ - extract::{Path, State}, http::StatusCode, response::IntoResponse, Extension, Json + Extension, Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, }; use serde::Deserialize; use crate::{ - api::v1::auth::CurrentUser, error::Error, objects::{Channel, Member, Permissions}, utils::{global_checks, order_by_is_above}, AppState + AppState, + api::v1::auth::CurrentUser, + error::Error, + objects::{Channel, Member, Permissions}, + utils::{global_checks, order_by_is_above}, }; #[derive(Deserialize)] @@ -55,7 +62,8 @@ pub async fn create( ) -> Result { global_checks(&app_state, uuid).await?; - let member = Member::check_membership(&mut app_state.pool.get().await?, uuid, guild_uuid).await?; + let member = + Member::check_membership(&mut app_state.pool.get().await?, uuid, guild_uuid).await?; member .check_permission(&app_state, Permissions::ManageChannel) diff --git a/src/api/v1/guilds/uuid/invites/mod.rs b/src/api/v1/guilds/uuid/invites/mod.rs index 2070452..649fc16 100644 --- a/src/api/v1/guilds/uuid/invites/mod.rs +++ b/src/api/v1/guilds/uuid/invites/mod.rs @@ -1,7 +1,10 @@ use std::sync::Arc; use axum::{ - extract::{Path, State}, http::StatusCode, response::IntoResponse, Extension, Json + Extension, Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, }; use serde::Deserialize; use uuid::Uuid; diff --git a/src/api/v1/guilds/uuid/members.rs b/src/api/v1/guilds/uuid/members.rs index 6c8b980..3ae10f7 100644 --- a/src/api/v1/guilds/uuid/members.rs +++ b/src/api/v1/guilds/uuid/members.rs @@ -2,7 +2,10 @@ use std::sync::Arc; use ::uuid::Uuid; use axum::{ - extract::{Path, State}, http::StatusCode, response::IntoResponse, Extension, Json + Extension, Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, }; use crate::{ diff --git a/src/api/v1/guilds/uuid/mod.rs b/src/api/v1/guilds/uuid/mod.rs index c5a809f..52f0b64 100644 --- a/src/api/v1/guilds/uuid/mod.rs +++ b/src/api/v1/guilds/uuid/mod.rs @@ -3,7 +3,11 @@ use std::sync::Arc; use axum::{ - extract::{Multipart, Path, State}, http::StatusCode, response::IntoResponse, routing::{get, patch, post}, Extension, Json, Router + Extension, Json, Router, + extract::{Multipart, Path, State}, + http::StatusCode, + response::IntoResponse, + routing::{get, patch, post}, }; use bytes::Bytes; use uuid::Uuid; diff --git a/src/api/v1/guilds/uuid/roles/mod.rs b/src/api/v1/guilds/uuid/roles/mod.rs index 5331143..820ef0d 100644 --- a/src/api/v1/guilds/uuid/roles/mod.rs +++ b/src/api/v1/guilds/uuid/roles/mod.rs @@ -2,7 +2,10 @@ use std::sync::Arc; use ::uuid::Uuid; use axum::{ - extract::{Path, State}, http::StatusCode, response::IntoResponse, Extension, Json + Extension, Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, }; use serde::Deserialize; diff --git a/src/api/v1/guilds/uuid/roles/uuid.rs b/src/api/v1/guilds/uuid/roles/uuid.rs index 91300bf..06193a1 100644 --- a/src/api/v1/guilds/uuid/roles/uuid.rs +++ b/src/api/v1/guilds/uuid/roles/uuid.rs @@ -2,7 +2,10 @@ use std::sync::Arc; use ::uuid::Uuid; use axum::{ - extract::{Path, State}, http::StatusCode, response::IntoResponse, Extension, Json + Extension, Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, }; use crate::{ diff --git a/src/api/v1/invites/id.rs b/src/api/v1/invites/id.rs index c752177..72ceea4 100644 --- a/src/api/v1/invites/id.rs +++ b/src/api/v1/invites/id.rs @@ -1,7 +1,10 @@ use std::sync::Arc; use axum::{ - extract::{Path, State}, http::StatusCode, response::IntoResponse, Extension, Json + Extension, Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, }; use uuid::Uuid; diff --git a/src/api/v1/me/friends/mod.rs b/src/api/v1/me/friends/mod.rs index 63284a8..a56f8d4 100644 --- a/src/api/v1/me/friends/mod.rs +++ b/src/api/v1/me/friends/mod.rs @@ -1,13 +1,17 @@ use std::sync::Arc; -use axum::{extract::State, http::StatusCode, response::IntoResponse, Extension, Json}; -use serde::Deserialize; use ::uuid::Uuid; +use axum::{Extension, Json, extract::State, http::StatusCode, response::IntoResponse}; +use serde::Deserialize; pub mod uuid; use crate::{ - api::v1::auth::CurrentUser, error::Error, objects::Me, utils::{global_checks, user_uuid_from_username}, AppState + AppState, + api::v1::auth::CurrentUser, + error::Error, + objects::Me, + utils::{global_checks, user_uuid_from_username}, }; /// Returns a list of users that are your friends diff --git a/src/api/v1/me/friends/uuid.rs b/src/api/v1/me/friends/uuid.rs index 5a32386..5367435 100644 --- a/src/api/v1/me/friends/uuid.rs +++ b/src/api/v1/me/friends/uuid.rs @@ -1,9 +1,10 @@ use std::sync::Arc; use axum::{ + Extension, extract::{Path, State}, http::StatusCode, - response::IntoResponse, Extension, + response::IntoResponse, }; use uuid::Uuid; diff --git a/src/api/v1/me/guilds.rs b/src/api/v1/me/guilds.rs index a2d2111..88dfad9 100644 --- a/src/api/v1/me/guilds.rs +++ b/src/api/v1/me/guilds.rs @@ -2,7 +2,7 @@ use std::sync::Arc; -use axum::{extract::State, http::StatusCode, response::IntoResponse, Extension, Json}; +use axum::{Extension, Json, extract::State, http::StatusCode, response::IntoResponse}; use uuid::Uuid; use crate::{ diff --git a/src/api/v1/me/mod.rs b/src/api/v1/me/mod.rs index ce577d4..e167d14 100644 --- a/src/api/v1/me/mod.rs +++ b/src/api/v1/me/mod.rs @@ -1,14 +1,18 @@ use std::sync::Arc; use axum::{ - extract::{DefaultBodyLimit, Multipart, State}, http::StatusCode, response::IntoResponse, routing::{delete, get, patch, post}, Extension, Json, Router + Extension, Json, Router, + extract::{DefaultBodyLimit, Multipart, State}, + http::StatusCode, + response::IntoResponse, + routing::{delete, get, patch, post}, }; use bytes::Bytes; use serde::Deserialize; use uuid::Uuid; use crate::{ - api::v1::auth::CurrentUser, error::Error, objects::Me, utils::global_checks, AppState + AppState, api::v1::auth::CurrentUser, error::Error, objects::Me, utils::global_checks, }; mod friends; diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs index f3e4305..5ca9558 100644 --- a/src/api/v1/mod.rs +++ b/src/api/v1/mod.rs @@ -2,9 +2,9 @@ use std::sync::Arc; -use axum::{middleware::from_fn_with_state, routing::get, Router}; +use axum::{Router, middleware::from_fn_with_state, routing::get}; -use crate::{api::v1::auth::CurrentUser, AppState}; +use crate::{AppState, api::v1::auth::CurrentUser}; mod auth; mod channels; @@ -21,7 +21,10 @@ pub fn router(app_state: Arc) -> Router> { .nest("/guilds", guilds::router()) .nest("/invites", invites::router()) .nest("/me", me::router()) - .layer(from_fn_with_state(app_state.clone(), CurrentUser::check_auth_layer)); + .layer(from_fn_with_state( + app_state.clone(), + CurrentUser::check_auth_layer, + )); Router::new() .route("/stats", get(stats::res)) diff --git a/src/api/v1/users/mod.rs b/src/api/v1/users/mod.rs index 82f2125..a4b93ce 100644 --- a/src/api/v1/users/mod.rs +++ b/src/api/v1/users/mod.rs @@ -2,13 +2,21 @@ use std::sync::Arc; -use axum::{ - extract::{Query, State}, http::StatusCode, response::IntoResponse, routing::get, Extension, Json, Router -}; use ::uuid::Uuid; +use axum::{ + Extension, Json, Router, + extract::{Query, State}, + http::StatusCode, + response::IntoResponse, + routing::get, +}; use crate::{ - api::v1::auth::CurrentUser, error::Error, objects::{StartAmountQuery, User}, utils::global_checks, AppState + AppState, + api::v1::auth::CurrentUser, + error::Error, + objects::{StartAmountQuery, User}, + utils::global_checks, }; mod uuid; diff --git a/src/api/v1/users/uuid.rs b/src/api/v1/users/uuid.rs index 2bdcfac..cee6df0 100644 --- a/src/api/v1/users/uuid.rs +++ b/src/api/v1/users/uuid.rs @@ -3,12 +3,19 @@ use std::sync::Arc; use axum::{ - extract::{Path, State}, http::StatusCode, response::IntoResponse, Extension, Json + Extension, Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, }; use uuid::Uuid; use crate::{ - api::v1::auth::CurrentUser, error::Error, objects::{Me, User}, utils::global_checks, AppState + AppState, + api::v1::auth::CurrentUser, + error::Error, + objects::{Me, User}, + utils::global_checks, }; /// `GET /api/v1/users/{uuid}` Returns user with the given UUID diff --git a/src/main.rs b/src/main.rs index ffbfa4e..e42c8dc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,29 +1,32 @@ use argon2::Argon2; -use axum::{http::{header, Method}, Router}; +use axum::{ + Router, + http::{Method, header}, +}; use clap::Parser; +use config::{Config, ConfigBuilder}; use diesel_async::pooled_connection::AsyncDieselConnectionManager; use diesel_async::pooled_connection::deadpool::Pool; +use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations}; use error::Error; use objects::MailClient; use socketioxide::SocketIo; use std::{sync::Arc, time::SystemTime}; use tower_http::cors::{AllowOrigin, CorsLayer}; -use config::{Config, ConfigBuilder}; -use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations}; pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); type Conn = deadpool::managed::Object>; -mod config; -mod wordlist; mod api; +mod config; pub mod error; pub mod objects; pub mod schema; -pub mod utils; mod socket; +pub mod utils; +mod wordlist; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] @@ -129,9 +132,7 @@ async fn main() -> Result<(), Error> { let cors = CorsLayer::new() // Allow any origin (equivalent to allowed_origin_fn returning true) - .allow_origin(AllowOrigin::predicate(|_origin, _request_head| { - true - })) + .allow_origin(AllowOrigin::predicate(|_origin, _request_head| true)) .allow_methods(vec![ Method::GET, Method::POST, @@ -157,14 +158,19 @@ async fn main() -> Result<(), Error> { // Allow credentials .allow_credentials(true); - let (socket_io, io) = SocketIo::builder().with_state(app_state.clone()).build_layer(); + let (socket_io, io) = SocketIo::builder() + .with_state(app_state.clone()) + .build_layer(); io.ns("/", socket::on_connect); // build our application with a route let app = Router::new() // `GET /` goes to `root` - .merge(api::router(web.backend_url.path().trim_end_matches("/"), app_state.clone())) + .merge(api::router( + web.backend_url.path().trim_end_matches("/"), + app_state.clone(), + )) .with_state(app_state) .layer(cors) .layer(socket_io); diff --git a/src/socket.rs b/src/socket.rs index e00a7c0..3fcae32 100644 --- a/src/socket.rs +++ b/src/socket.rs @@ -2,24 +2,26 @@ use std::sync::Arc; use log::info; use rmpv::Value; -use socketioxide::{ - extract::{AckSender, Data, SocketRef, State}, -}; +use socketioxide::extract::{AckSender, Data, SocketRef, State}; use crate::AppState; -pub async fn on_connect(State(app_state): State>, socket: SocketRef, Data(data): Data) { +pub async fn on_connect( + State(_app_state): State>, + socket: SocketRef, + Data(data): Data, +) { socket.emit("auth", &data).ok(); socket.on("message", async |socket: SocketRef, Data::(data)| { - info!("{}", data); + info!("{data}"); socket.emit("message-back", &data).ok(); }); socket.on( "message-with-ack", async |Data::(data), ack: AckSender| { - info!("{}", data); + info!("{data}"); ack.send(&data).ok(); }, ); diff --git a/src/utils.rs b/src/utils.rs index ac8e343..e1df906 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,5 @@ +use rand::seq::IndexedRandom; use std::sync::LazyLock; -use rand::{seq::IndexedRandom}; use axum::body::Bytes; use axum_extra::extract::cookie::{Cookie, SameSite}; @@ -20,7 +20,7 @@ use crate::{ error::Error, objects::{HasIsAbove, HasUuid}, schema::users, - wordlist::{ADJECTIVES, ANIMALS} + wordlist::{ADJECTIVES, ANIMALS}, }; pub static EMAIL_REGEX: LazyLock = LazyLock::new(|| { @@ -216,5 +216,5 @@ pub fn generate_device_name() -> String { let adjective = ADJECTIVES.choose(&mut rng).unwrap(); let animal = ANIMALS.choose(&mut rng).unwrap(); - return [*adjective, *animal].join(" ") + [*adjective, *animal].join(" ") } diff --git a/src/wordlist.rs b/src/wordlist.rs index 0c17723..1227c1f 100644 --- a/src/wordlist.rs +++ b/src/wordlist.rs @@ -1,4 +1,4 @@ -pub const ANIMALS: [&'static str; 223] = [ +pub const ANIMALS: [&str; 223] = [ "Aardvark", "Albatross", "Alligator", @@ -224,7 +224,7 @@ pub const ANIMALS: [&'static str; 223] = [ "Zebra", ]; -pub const ADJECTIVES: [&'static str; 765] = [ +pub const ADJECTIVES: [&str; 765] = [ "Other", "Such", "First", @@ -990,4 +990,4 @@ pub const ADJECTIVES: [&'static str; 765] = [ "Vocal", "Obscure", "Innovative", -]; \ No newline at end of file +]; From 2fb7e7781f42c419c0d9f31de04b5a784e4c11f7 Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 20 Jul 2025 18:11:08 +0200 Subject: [PATCH 107/160] feat: reimplement old websocket --- Cargo.toml | 2 +- src/api/v1/channels/mod.rs | 3 +- src/api/v1/channels/uuid/mod.rs | 2 +- src/api/v1/channels/uuid/socket.rs | 147 ++++++++++++++++------------- src/api/v1/mod.rs | 4 +- src/error.rs | 3 + 6 files changed, 91 insertions(+), 70 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2b9962c..e0c83bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,7 @@ bindet = "0.3.2" bunny-api-tokio = { version = "0.4", features = ["edge_storage"], default-features = false } # Web Server -axum = { version = "0.8.4", features = ["macros", "multipart"] } +axum = { version = "0.8.4", features = ["macros", "multipart", "ws"] } tower-http = { version = "0.6.6", features = ["cors"] } axum-extra = { version = "0.10.1", features = ["cookie", "typed-header"] } socketioxide = { version = "0.17.2", features = ["state"] } diff --git a/src/api/v1/channels/mod.rs b/src/api/v1/channels/mod.rs index 24b62f7..cc033af 100644 --- a/src/api/v1/channels/mod.rs +++ b/src/api/v1/channels/mod.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use axum::{ Router, - routing::{delete, get, patch}, + routing::{any, delete, get, patch}, }; //use socketioxide::SocketIo; @@ -15,5 +15,6 @@ pub fn router() -> Router> { .route("/{uuid}", get(uuid::get)) .route("/{uuid}", delete(uuid::delete)) .route("/{uuid}", patch(uuid::patch)) + .route("/{uuid}/socket", any(uuid::socket::ws)) .route("/{uuid}/messages", get(uuid::messages::get)) } diff --git a/src/api/v1/channels/uuid/mod.rs b/src/api/v1/channels/uuid/mod.rs index 5c88a29..373742e 100644 --- a/src/api/v1/channels/uuid/mod.rs +++ b/src/api/v1/channels/uuid/mod.rs @@ -1,7 +1,7 @@ //! `/api/v1/channels/{uuid}` Channel specific endpoints pub mod messages; -//pub mod socket; +pub mod socket; use std::sync::Arc; diff --git a/src/api/v1/channels/uuid/socket.rs b/src/api/v1/channels/uuid/socket.rs index 7233f39..46a7334 100644 --- a/src/api/v1/channels/uuid/socket.rs +++ b/src/api/v1/channels/uuid/socket.rs @@ -1,18 +1,21 @@ -use actix_web::{ - Error, HttpRequest, HttpResponse, get, - http::header::{HeaderValue, SEC_WEBSOCKET_PROTOCOL}, - rt, web, +use std::sync::Arc; + +use axum::{ + extract::{Path, State, WebSocketUpgrade, ws::Message}, + http::HeaderMap, + response::IntoResponse, }; -use actix_ws::AggregatedMessage; +use futures::SinkExt; use futures_util::StreamExt as _; use serde::Deserialize; use uuid::Uuid; use crate::{ - Data, + AppState, api::v1::auth::check_access_token, + error::Error, objects::{Channel, Member}, - utils::{get_ws_protocol_header, global_checks}, + utils::global_checks, }; #[derive(Deserialize)] @@ -21,100 +24,114 @@ struct MessageBody { reply_to: Option, } -#[get("/{uuid}/socket")] pub async fn ws( - req: HttpRequest, - path: web::Path<(Uuid,)>, - stream: web::Payload, - data: web::Data, -) -> Result { - // Get all headers - let headers = req.headers(); - + ws: WebSocketUpgrade, + State(app_state): State>, + Path(channel_uuid): Path, + headers: HeaderMap, +) -> Result { // Retrieve auth header - let auth_header = get_ws_protocol_header(headers)?; + let auth_token = headers.get(axum::http::header::SEC_WEBSOCKET_PROTOCOL); - // Get uuid from path - let channel_uuid = path.into_inner().0; + if auth_token.is_none() { + return Err(Error::Unauthorized( + "No authorization header provided".to_string(), + )); + } - let mut conn = data.pool.get().await.map_err(crate::error::Error::from)?; + let auth_raw = auth_token.unwrap().to_str()?; + + let mut auth = auth_raw.split_whitespace(); + + let response_proto = auth.next(); + + let auth_value = auth.next(); + + if response_proto.is_none() { + return Err(Error::BadRequest( + "Sec-WebSocket-Protocol header is empty".to_string(), + )); + } else if response_proto.is_some_and(|rp| rp != "Authorization,") { + return Err(Error::BadRequest( + "First protocol should be Authorization".to_string(), + )); + } + + if auth_value.is_none() { + return Err(Error::BadRequest("No token provided".to_string())); + } + + let auth_header = auth_value.unwrap(); + + let mut conn = app_state + .pool + .get() + .await + .map_err(crate::error::Error::from)?; // Authorize client using auth header let uuid = check_access_token(auth_header, &mut conn).await?; - global_checks(&data, uuid).await?; + global_checks(&app_state, uuid).await?; - let channel = Channel::fetch_one(&data, channel_uuid).await?; + let channel = Channel::fetch_one(&app_state, channel_uuid).await?; Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; - let (mut res, mut session_1, stream) = actix_ws::handle(&req, stream)?; - - let mut stream = stream - .aggregate_continuations() - // aggregate continuation frames up to 1MiB - .max_continuation_size(2_usize.pow(20)); - - let mut pubsub = data + let mut pubsub = app_state .cache_pool .get_async_pubsub() .await .map_err(crate::error::Error::from)?; - let mut session_2 = session_1.clone(); + let mut res = ws.on_upgrade(async move |socket| { + let (mut sender, mut receiver) = socket.split(); - rt::spawn(async move { - pubsub.subscribe(channel_uuid.to_string()).await?; - while let Some(msg) = pubsub.on_message().next().await { - let payload: String = msg.get_payload()?; - session_1.text(payload).await?; - } + tokio::spawn(async move { + pubsub.subscribe(channel_uuid.to_string()).await?; + while let Some(msg) = pubsub.on_message().next().await { + let payload: String = msg.get_payload()?; + sender.send(payload.into()).await?; + } - Ok::<(), crate::error::Error>(()) - }); - - // start task but don't wait for it - rt::spawn(async move { - // receive messages from websocket - while let Some(msg) = stream.next().await { - match msg { - Ok(AggregatedMessage::Text(text)) => { - let mut conn = data.cache_pool.get_multiplexed_tokio_connection().await?; + Ok::<(), crate::error::Error>(()) + }); + tokio::spawn(async move { + while let Some(msg) = receiver.next().await { + if let Ok(Message::Text(text)) = msg { let message_body: MessageBody = serde_json::from_str(&text)?; let message = channel - .new_message(&data, uuid, message_body.message, message_body.reply_to) + .new_message( + &app_state, + uuid, + message_body.message, + message_body.reply_to, + ) .await?; redis::cmd("PUBLISH") .arg(&[channel_uuid.to_string(), serde_json::to_string(&message)?]) - .exec_async(&mut conn) + .exec_async( + &mut app_state + .cache_pool + .get_multiplexed_tokio_connection() + .await?, + ) .await?; } - - Ok(AggregatedMessage::Binary(bin)) => { - // echo binary message - session_2.binary(bin).await?; - } - - Ok(AggregatedMessage::Ping(msg)) => { - // respond to PING frame with PONG frame - session_2.pong(&msg).await?; - } - - _ => {} } - } - Ok::<(), crate::error::Error>(()) + Ok::<(), crate::error::Error>(()) + }); }); let headers = res.headers_mut(); headers.append( - SEC_WEBSOCKET_PROTOCOL, - HeaderValue::from_str("Authorization")?, + axum::http::header::SEC_WEBSOCKET_PROTOCOL, + "Authorization".parse()?, ); // respond immediately with response connected to WS session diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs index 5ca9558..860944c 100644 --- a/src/api/v1/mod.rs +++ b/src/api/v1/mod.rs @@ -17,7 +17,6 @@ mod users; pub fn router(app_state: Arc) -> Router> { let router_with_auth = Router::new() .nest("/users", users::router()) - .nest("/channels", channels::router()) .nest("/guilds", guilds::router()) .nest("/invites", invites::router()) .nest("/me", me::router()) @@ -28,6 +27,7 @@ pub fn router(app_state: Arc) -> Router> { Router::new() .route("/stats", get(stats::res)) - .nest("/auth", auth::router(app_state)) + .nest("/auth", auth::router(app_state.clone())) + .nest("/channels", channels::router(app_state)) .merge(router_with_auth) } diff --git a/src/error.rs b/src/error.rs index 1b8f27c..d6f7a12 100644 --- a/src/error.rs +++ b/src/error.rs @@ -83,6 +83,9 @@ pub enum Error { TooManyRequests(String), #[error("{0}")] InternalServerError(String), + // TODO: remove when doing socket.io + #[error(transparent)] + AxumError(#[from] axum::Error), } impl IntoResponse for Error { From 8ec1610b2e8e08a9af76cc086b544e8fc68a6501 Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 20 Jul 2025 18:11:31 +0200 Subject: [PATCH 108/160] feat: remove dependency on socket.io Keeping stuff commented so we can revisit, currently just need a working version --- Cargo.toml | 4 ++-- src/api/v1/auth/revoke.rs | 1 - src/api/v1/channels/mod.rs | 15 +++++++++------ src/main.rs | 17 +++++------------ 4 files changed, 16 insertions(+), 21 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e0c83bb..3decea6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,10 +38,10 @@ bindet = "0.3.2" bunny-api-tokio = { version = "0.4", features = ["edge_storage"], default-features = false } # Web Server -axum = { version = "0.8.4", features = ["macros", "multipart", "ws"] } +axum = { version = "0.8.4", features = ["multipart", "ws"] } tower-http = { version = "0.6.6", features = ["cors"] } axum-extra = { version = "0.10.1", features = ["cookie", "typed-header"] } -socketioxide = { version = "0.17.2", features = ["state"] } +#socketioxide = { version = "0.17.2", features = ["state"] } url = { version = "2.5", features = ["serde"] } time = "0.3.41" diff --git a/src/api/v1/auth/revoke.rs b/src/api/v1/auth/revoke.rs index dd87ec3..90b96ae 100644 --- a/src/api/v1/auth/revoke.rs +++ b/src/api/v1/auth/revoke.rs @@ -24,7 +24,6 @@ pub struct RevokeRequest { } // TODO: Should maybe be a delete request? -#[axum::debug_handler] pub async fn post( State(app_state): State>, Extension(CurrentUser(uuid)): Extension>, diff --git a/src/api/v1/channels/mod.rs b/src/api/v1/channels/mod.rs index cc033af..41d029a 100644 --- a/src/api/v1/channels/mod.rs +++ b/src/api/v1/channels/mod.rs @@ -1,20 +1,23 @@ use std::sync::Arc; use axum::{ - Router, - routing::{any, delete, get, patch}, + middleware::from_fn_with_state, routing::{any, delete, get, patch}, Router }; //use socketioxide::SocketIo; -use crate::AppState; +use crate::{api::v1::auth::CurrentUser, AppState}; mod uuid; -pub fn router() -> Router> { - Router::new() +pub fn router(app_state: Arc) -> Router> { + let router_with_auth = Router::new() .route("/{uuid}", get(uuid::get)) .route("/{uuid}", delete(uuid::delete)) .route("/{uuid}", patch(uuid::patch)) - .route("/{uuid}/socket", any(uuid::socket::ws)) .route("/{uuid}/messages", get(uuid::messages::get)) + .layer(from_fn_with_state(app_state, CurrentUser::check_auth_layer)); + + Router::new() + .route("/{uuid}/socket", any(uuid::socket::ws)) + .merge(router_with_auth) } diff --git a/src/main.rs b/src/main.rs index e42c8dc..13e661d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,6 @@ use diesel_async::pooled_connection::deadpool::Pool; use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations}; use error::Error; use objects::MailClient; -use socketioxide::SocketIo; use std::{sync::Arc, time::SystemTime}; use tower_http::cors::{AllowOrigin, CorsLayer}; @@ -24,7 +23,7 @@ mod config; pub mod error; pub mod objects; pub mod schema; -mod socket; +//mod socket; pub mod utils; mod wordlist; @@ -53,12 +52,6 @@ pub struct AppState { async fn main() -> Result<(), Error> { tracing_subscriber::fmt::init(); - //SimpleLogger::new() - // .with_level(log::LevelFilter::Info) - // .with_colors(true) - // .env() - // .init() - // .unwrap(); let args = Args::parse(); let config = ConfigBuilder::load(args.config).await?.build(); @@ -158,12 +151,12 @@ async fn main() -> Result<(), Error> { // Allow credentials .allow_credentials(true); - let (socket_io, io) = SocketIo::builder() + /*let (socket_io, io) = SocketIo::builder() .with_state(app_state.clone()) .build_layer(); io.ns("/", socket::on_connect); - + */ // build our application with a route let app = Router::new() // `GET /` goes to `root` @@ -172,8 +165,8 @@ async fn main() -> Result<(), Error> { app_state.clone(), )) .with_state(app_state) - .layer(cors) - .layer(socket_io); + //.layer(socket_io) + .layer(cors); // run our app with hyper, listening globally on port 3000 let listener = tokio::net::TcpListener::bind(web.ip + ":" + &web.port.to_string()).await?; From 1c07957c4e77422a472dea3538e66499dc37ab9f Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 20 Jul 2025 18:45:50 +0200 Subject: [PATCH 109/160] refactor: small dependency optimizations --- Cargo.toml | 6 +----- src/api/v1/channels/uuid/socket.rs | 3 +-- src/objects/channel.rs | 4 ++-- src/objects/guild.rs | 2 +- src/objects/me.rs | 4 ++-- 5 files changed, 7 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3decea6..cdbcc0f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,6 @@ clap = { version = "4.5", features = ["derive"] } log = "0.4" # async -futures = "0.3" tokio = { version = "1.46", features = ["full"] } futures-util = "0.3.31" @@ -31,7 +30,6 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" toml = "0.9" bytes = "1.10.1" -rmpv = { version = "1.3.0", features = ["with-serde"] } # File Storage bindet = "0.3.2" @@ -39,8 +37,8 @@ bunny-api-tokio = { version = "0.4", features = ["edge_storage"], default-featur # Web Server axum = { version = "0.8.4", features = ["multipart", "ws"] } -tower-http = { version = "0.6.6", features = ["cors"] } axum-extra = { version = "0.10.1", features = ["cookie", "typed-header"] } +tower-http = { version = "0.6.6", features = ["cors"] } #socketioxide = { version = "0.17.2", features = ["state"] } url = { version = "2.5", features = ["serde"] } time = "0.3.41" @@ -63,5 +61,3 @@ lettre = { version = "0.11", features = ["tokio1", "tokio1-native-tls"] } chrono = { version = "0.4.41", features = ["serde"] } tracing-subscriber = "0.3.19" rand = "0.9.1" - - diff --git a/src/api/v1/channels/uuid/socket.rs b/src/api/v1/channels/uuid/socket.rs index 46a7334..dd020e3 100644 --- a/src/api/v1/channels/uuid/socket.rs +++ b/src/api/v1/channels/uuid/socket.rs @@ -5,8 +5,7 @@ use axum::{ http::HeaderMap, response::IntoResponse, }; -use futures::SinkExt; -use futures_util::StreamExt as _; +use futures_util::{SinkExt, StreamExt}; use serde::Deserialize; use uuid::Uuid; diff --git a/src/objects/channel.rs b/src/objects/channel.rs index 3b34ac6..cacb153 100644 --- a/src/objects/channel.rs +++ b/src/objects/channel.rs @@ -102,7 +102,7 @@ impl Channel { c.clone().build(&mut conn).await }); - futures::future::try_join_all(channel_futures).await + futures_util::future::try_join_all(channel_futures).await } pub async fn fetch_one(app_state: &AppState, channel_uuid: Uuid) -> Result { @@ -267,7 +267,7 @@ impl Channel { let message_futures = messages.iter().map(async move |b| b.build(app_state).await); - futures::future::try_join_all(message_futures).await + futures_util::future::try_join_all(message_futures).await } pub async fn new_message( diff --git a/src/objects/guild.rs b/src/objects/guild.rs index e27e129..9514e49 100644 --- a/src/objects/guild.rs +++ b/src/objects/guild.rs @@ -96,7 +96,7 @@ impl Guild { }); // Execute all futures concurrently and collect results - futures::future::try_join_all(guild_futures).await + futures_util::future::try_join_all(guild_futures).await } pub async fn new(conn: &mut Conn, name: String, owner_uuid: Uuid) -> Result { diff --git a/src/objects/me.rs b/src/objects/me.rs index 3b51da4..a0b399d 100644 --- a/src/objects/me.rs +++ b/src/objects/me.rs @@ -391,13 +391,13 @@ impl Me { User::fetch_one_with_friendship(app_state, self, friend.uuid2).await }); - let mut friends = futures::future::try_join_all(friend_futures).await?; + let mut friends = futures_util::future::try_join_all(friend_futures).await?; let friend_futures = friends2.iter().map(async move |friend| { User::fetch_one_with_friendship(app_state, self, friend.uuid1).await }); - friends.append(&mut futures::future::try_join_all(friend_futures).await?); + friends.append(&mut futures_util::future::try_join_all(friend_futures).await?); Ok(friends) } From 8a58774359c2a5f1ac9d1f862b42f1f800175842 Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 20 Jul 2025 20:25:15 +0200 Subject: [PATCH 110/160] fix: get cache correctly from redis --- src/api/v1/channels/mod.rs | 6 ++++-- src/api/v1/guilds/uuid/channels.rs | 3 ++- src/api/v1/guilds/uuid/roles/mod.rs | 6 ++++-- src/api/v1/guilds/uuid/roles/uuid.rs | 6 ++++-- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/api/v1/channels/mod.rs b/src/api/v1/channels/mod.rs index 41d029a..fa90ccd 100644 --- a/src/api/v1/channels/mod.rs +++ b/src/api/v1/channels/mod.rs @@ -1,11 +1,13 @@ use std::sync::Arc; use axum::{ - middleware::from_fn_with_state, routing::{any, delete, get, patch}, Router + Router, + middleware::from_fn_with_state, + routing::{any, delete, get, patch}, }; //use socketioxide::SocketIo; -use crate::{api::v1::auth::CurrentUser, AppState}; +use crate::{AppState, api::v1::auth::CurrentUser}; mod uuid; diff --git a/src/api/v1/guilds/uuid/channels.rs b/src/api/v1/guilds/uuid/channels.rs index 836982d..82368b9 100644 --- a/src/api/v1/guilds/uuid/channels.rs +++ b/src/api/v1/guilds/uuid/channels.rs @@ -35,8 +35,9 @@ pub async fn get( if let Ok(cache_hit) = app_state .get_cache_key(format!("{guild_uuid}_channels")) .await + && let Ok(channels) = serde_json::from_str::>(&cache_hit) { - return Ok((StatusCode::OK, Json(cache_hit)).into_response()); + return Ok((StatusCode::OK, Json(channels)).into_response()); } let channels = Channel::fetch_all(&app_state.pool, guild_uuid).await?; diff --git a/src/api/v1/guilds/uuid/roles/mod.rs b/src/api/v1/guilds/uuid/roles/mod.rs index 820ef0d..0e496a0 100644 --- a/src/api/v1/guilds/uuid/roles/mod.rs +++ b/src/api/v1/guilds/uuid/roles/mod.rs @@ -35,8 +35,10 @@ pub async fn get( Member::check_membership(&mut conn, uuid, guild_uuid).await?; - if let Ok(cache_hit) = app_state.get_cache_key(format!("{guild_uuid}_roles")).await { - return Ok((StatusCode::OK, Json(cache_hit)).into_response()); + if let Ok(cache_hit) = app_state.get_cache_key(format!("{guild_uuid}_roles")).await + && let Ok(roles) = serde_json::from_str::>(&cache_hit) + { + return Ok((StatusCode::OK, Json(roles)).into_response()); } let roles = Role::fetch_all(&mut conn, guild_uuid).await?; diff --git a/src/api/v1/guilds/uuid/roles/uuid.rs b/src/api/v1/guilds/uuid/roles/uuid.rs index 06193a1..732d553 100644 --- a/src/api/v1/guilds/uuid/roles/uuid.rs +++ b/src/api/v1/guilds/uuid/roles/uuid.rs @@ -27,8 +27,10 @@ pub async fn get( Member::check_membership(&mut conn, uuid, guild_uuid).await?; - if let Ok(cache_hit) = app_state.get_cache_key(format!("{role_uuid}")).await { - return Ok((StatusCode::OK, Json(cache_hit)).into_response()); + if let Ok(cache_hit) = app_state.get_cache_key(format!("{role_uuid}")).await + && let Ok(role) = serde_json::from_str::(&cache_hit) + { + return Ok((StatusCode::OK, Json(role)).into_response()); } let role = Role::fetch_one(&mut conn, role_uuid).await?; From f5d4211fadd8a0744b0445e29f901e4c3760428c Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 20 Jul 2025 20:29:28 +0200 Subject: [PATCH 111/160] fix: force rust 1.88 in builds --- .woodpecker/build-and-publish.yml | 4 ++-- .woodpecker/publish-docs.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.woodpecker/build-and-publish.yml b/.woodpecker/build-and-publish.yml index 4e263a9..66f377d 100644 --- a/.woodpecker/build-and-publish.yml +++ b/.woodpecker/build-and-publish.yml @@ -1,6 +1,6 @@ steps: - name: build-x86_64 - image: rust:bookworm + image: rust:1.88-bookworm commands: - cargo build --release when: @@ -8,7 +8,7 @@ steps: - event: pull_request - name: build-arm64 - image: rust:bookworm + image: rust:1.88-bookworm commands: - dpkg --add-architecture arm64 - apt-get update -y && apt-get install -y crossbuild-essential-arm64 libssl-dev:arm64 diff --git a/.woodpecker/publish-docs.yml b/.woodpecker/publish-docs.yml index e6ce482..7744dc7 100644 --- a/.woodpecker/publish-docs.yml +++ b/.woodpecker/publish-docs.yml @@ -4,7 +4,7 @@ when: steps: - name: build-docs - image: rust:bookworm + image: rust:1.88-bookworm commands: - cargo doc --release --no-deps From fa52412b433bd5c48027c5bb65dbeb735c0de730 Mon Sep 17 00:00:00 2001 From: Radical Date: Mon, 21 Jul 2025 04:15:04 +0200 Subject: [PATCH 112/160] feat: make database access more uniform and stop locking as many pool connections --- src/api/v1/auth/register.rs | 2 +- src/api/v1/auth/reset_password.rs | 22 ++- src/api/v1/auth/verify_email.rs | 8 +- src/api/v1/channels/uuid/messages.rs | 15 +- src/api/v1/channels/uuid/mod.rs | 46 +++--- src/api/v1/channels/uuid/socket.rs | 7 +- src/api/v1/guilds/mod.rs | 6 +- src/api/v1/guilds/uuid/channels.rs | 29 ++-- src/api/v1/guilds/uuid/invites/mod.rs | 10 +- src/api/v1/guilds/uuid/members.rs | 6 +- src/api/v1/guilds/uuid/mod.rs | 19 +-- src/api/v1/guilds/uuid/roles/mod.rs | 21 +-- src/api/v1/guilds/uuid/roles/uuid.rs | 15 +- src/api/v1/invites/id.rs | 6 +- src/api/v1/me/friends/mod.rs | 12 +- src/api/v1/me/friends/uuid.rs | 4 +- src/api/v1/me/guilds.rs | 4 +- src/api/v1/me/mod.rs | 23 +-- src/api/v1/users/mod.rs | 6 +- src/api/v1/users/uuid.rs | 9 +- src/objects/channel.rs | 209 ++++++++++++++++---------- src/objects/email_token.rs | 21 +-- src/objects/guild.rs | 35 ++--- src/objects/me.rs | 127 +++++++++------- src/objects/member.rs | 45 +++--- src/objects/message.rs | 10 +- src/objects/password_reset_token.rs | 72 ++++----- src/objects/role.rs | 19 ++- src/objects/user.rs | 27 ++-- src/utils.rs | 54 ++++--- 30 files changed, 516 insertions(+), 373 deletions(-) diff --git a/src/api/v1/auth/register.rs b/src/api/v1/auth/register.rs index 807fab8..545e5aa 100644 --- a/src/api/v1/auth/register.rs +++ b/src/api/v1/auth/register.rs @@ -159,7 +159,7 @@ pub async fn post( .await?; if let Some(initial_guild) = app_state.config.instance.initial_guild { - Member::new(&app_state, uuid, initial_guild).await?; + Member::new(&mut conn, &app_state.cache_pool, uuid, initial_guild).await?; } let mut response = ( diff --git a/src/api/v1/auth/reset_password.rs b/src/api/v1/auth/reset_password.rs index bac465c..35c4b41 100644 --- a/src/api/v1/auth/reset_password.rs +++ b/src/api/v1/auth/reset_password.rs @@ -38,11 +38,17 @@ pub async fn get( State(app_state): State>, query: Query, ) -> Result { - if let Ok(password_reset_token) = - PasswordResetToken::get_with_identifier(&app_state, query.identifier.clone()).await + let mut conn = app_state.pool.get().await?; + + if let Ok(password_reset_token) = PasswordResetToken::get_with_identifier( + &mut conn, + &app_state.cache_pool, + query.identifier.clone(), + ) + .await { if Utc::now().signed_duration_since(password_reset_token.created_at) > Duration::hours(1) { - password_reset_token.delete(&app_state).await?; + password_reset_token.delete(&app_state.cache_pool).await?; } else { return Err(Error::TooManyRequests( "Please allow 1 hour before sending a new email".to_string(), @@ -50,7 +56,7 @@ pub async fn get( } } - PasswordResetToken::new(&app_state, query.identifier.clone()).await?; + PasswordResetToken::new(&mut conn, &app_state, query.identifier.clone()).await?; Ok(StatusCode::OK) } @@ -87,10 +93,14 @@ pub async fn post( reset_password: Json, ) -> Result { let password_reset_token = - PasswordResetToken::get(&app_state, reset_password.token.clone()).await?; + PasswordResetToken::get(&app_state.cache_pool, reset_password.token.clone()).await?; password_reset_token - .set_password(&app_state, reset_password.password.clone()) + .set_password( + &mut app_state.pool.get().await?, + &app_state, + reset_password.password.clone(), + ) .await?; Ok(StatusCode::OK) diff --git a/src/api/v1/auth/verify_email.rs b/src/api/v1/auth/verify_email.rs index 0801768..1cb8aef 100644 --- a/src/api/v1/auth/verify_email.rs +++ b/src/api/v1/auth/verify_email.rs @@ -55,7 +55,7 @@ pub async fn get( return Ok(StatusCode::NO_CONTENT); } - let email_token = EmailToken::get(&app_state, me.uuid).await?; + let email_token = EmailToken::get(&app_state.cache_pool, me.uuid).await?; if query.token != email_token.token { return Ok(StatusCode::UNAUTHORIZED); @@ -63,7 +63,7 @@ pub async fn get( me.verify_email(&mut conn).await?; - email_token.delete(&app_state).await?; + email_token.delete(&app_state.cache_pool).await?; Ok(StatusCode::OK) } @@ -91,9 +91,9 @@ pub async fn post( return Ok(StatusCode::NO_CONTENT); } - if let Ok(email_token) = EmailToken::get(&app_state, me.uuid).await { + if let Ok(email_token) = EmailToken::get(&app_state.cache_pool, me.uuid).await { if Utc::now().signed_duration_since(email_token.created_at) > Duration::hours(1) { - email_token.delete(&app_state).await?; + email_token.delete(&app_state.cache_pool).await?; } else { return Err(Error::TooManyRequests( "Please allow 1 hour before sending a new email".to_string(), diff --git a/src/api/v1/channels/uuid/messages.rs b/src/api/v1/channels/uuid/messages.rs index b8f0ad6..1f9010d 100644 --- a/src/api/v1/channels/uuid/messages.rs +++ b/src/api/v1/channels/uuid/messages.rs @@ -60,14 +60,21 @@ pub async fn get( Query(message_request): Query, Extension(CurrentUser(uuid)): Extension>, ) -> Result { - global_checks(&app_state, uuid).await?; + let mut conn = app_state.pool.get().await?; - let channel = Channel::fetch_one(&app_state, channel_uuid).await?; + global_checks(&mut conn, &app_state.config, uuid).await?; - Member::check_membership(&mut app_state.pool.get().await?, uuid, channel.guild_uuid).await?; + let channel = Channel::fetch_one(&mut conn, &app_state.cache_pool, channel_uuid).await?; + + Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; let messages = channel - .fetch_messages(&app_state, message_request.amount, message_request.offset) + .fetch_messages( + &mut conn, + &app_state.cache_pool, + message_request.amount, + message_request.offset, + ) .await?; Ok((StatusCode::OK, Json(messages))) diff --git a/src/api/v1/channels/uuid/mod.rs b/src/api/v1/channels/uuid/mod.rs index 373742e..f5566b3 100644 --- a/src/api/v1/channels/uuid/mod.rs +++ b/src/api/v1/channels/uuid/mod.rs @@ -27,11 +27,13 @@ pub async fn get( Path(channel_uuid): Path, Extension(CurrentUser(uuid)): Extension>, ) -> Result { - global_checks(&app_state, uuid).await?; + let mut conn = app_state.pool.get().await?; - let channel = Channel::fetch_one(&app_state, channel_uuid).await?; + global_checks(&mut conn, &app_state.config, uuid).await?; - Member::check_membership(&mut app_state.pool.get().await?, uuid, channel.guild_uuid).await?; + let channel = Channel::fetch_one(&mut conn, &app_state.cache_pool, channel_uuid).await?; + + Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; Ok((StatusCode::OK, Json(channel))) } @@ -41,19 +43,19 @@ pub async fn delete( Path(channel_uuid): Path, Extension(CurrentUser(uuid)): Extension>, ) -> Result { - global_checks(&app_state, uuid).await?; + let mut conn = app_state.pool.get().await?; - let channel = Channel::fetch_one(&app_state, channel_uuid).await?; + global_checks(&mut conn, &app_state.config, uuid).await?; - let member = - Member::check_membership(&mut app_state.pool.get().await?, uuid, channel.guild_uuid) - .await?; + let channel = Channel::fetch_one(&mut conn, &app_state.cache_pool, channel_uuid).await?; + + let member = Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; member - .check_permission(&app_state, Permissions::ManageChannel) + .check_permission(&mut conn, &app_state.cache_pool, Permissions::ManageChannel) .await?; - channel.delete(&app_state).await?; + channel.delete(&mut conn, &app_state.cache_pool).await?; Ok(StatusCode::OK) } @@ -102,31 +104,37 @@ pub async fn patch( Extension(CurrentUser(uuid)): Extension>, Json(new_info): Json, ) -> Result { - global_checks(&app_state, uuid).await?; + let mut conn = app_state.pool.get().await?; - let mut channel = Channel::fetch_one(&app_state, channel_uuid).await?; + global_checks(&mut conn, &app_state.config, uuid).await?; - let member = - Member::check_membership(&mut app_state.pool.get().await?, uuid, channel.guild_uuid) - .await?; + let mut channel = Channel::fetch_one(&mut conn, &app_state.cache_pool, channel_uuid).await?; + + let member = Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; member - .check_permission(&app_state, Permissions::ManageChannel) + .check_permission(&mut conn, &app_state.cache_pool, Permissions::ManageChannel) .await?; if let Some(new_name) = &new_info.name { - channel.set_name(&app_state, new_name.to_string()).await?; + channel + .set_name(&mut conn, &app_state.cache_pool, new_name.to_string()) + .await?; } if let Some(new_description) = &new_info.description { channel - .set_description(&app_state, new_description.to_string()) + .set_description( + &mut conn, + &app_state.cache_pool, + new_description.to_string(), + ) .await?; } if let Some(new_is_above) = &new_info.is_above { channel - .set_description(&app_state, new_is_above.to_string()) + .set_description(&mut conn, &app_state.cache_pool, new_is_above.to_string()) .await?; } diff --git a/src/api/v1/channels/uuid/socket.rs b/src/api/v1/channels/uuid/socket.rs index dd020e3..ac04301 100644 --- a/src/api/v1/channels/uuid/socket.rs +++ b/src/api/v1/channels/uuid/socket.rs @@ -71,9 +71,9 @@ pub async fn ws( // Authorize client using auth header let uuid = check_access_token(auth_header, &mut conn).await?; - global_checks(&app_state, uuid).await?; + global_checks(&mut conn, &app_state.config, uuid).await?; - let channel = Channel::fetch_one(&app_state, channel_uuid).await?; + let channel = Channel::fetch_one(&mut conn, &app_state.cache_pool, channel_uuid).await?; Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; @@ -103,7 +103,8 @@ pub async fn ws( let message = channel .new_message( - &app_state, + &mut conn, + &app_state.cache_pool, uuid, message_body.message, message_body.reply_to, diff --git a/src/api/v1/guilds/mod.rs b/src/api/v1/guilds/mod.rs index 8118522..5b9f089 100644 --- a/src/api/v1/guilds/mod.rs +++ b/src/api/v1/guilds/mod.rs @@ -128,9 +128,11 @@ pub async fn get_guilds( let start = request_query.start.unwrap_or(0); let amount = request_query.amount.unwrap_or(10); - global_checks(&app_state, uuid).await?; + let mut conn = app_state.pool.get().await?; - let guilds = Guild::fetch_amount(&app_state.pool, start, amount).await?; + global_checks(&mut conn, &app_state.config, uuid).await?; + + let guilds = Guild::fetch_amount(&mut conn, start, amount).await?; Ok((StatusCode::OK, Json(guilds))) } diff --git a/src/api/v1/guilds/uuid/channels.rs b/src/api/v1/guilds/uuid/channels.rs index 82368b9..1cd7f78 100644 --- a/src/api/v1/guilds/uuid/channels.rs +++ b/src/api/v1/guilds/uuid/channels.rs @@ -14,7 +14,7 @@ use crate::{ api::v1::auth::CurrentUser, error::Error, objects::{Channel, Member, Permissions}, - utils::{global_checks, order_by_is_above}, + utils::{CacheFns, global_checks, order_by_is_above}, }; #[derive(Deserialize)] @@ -28,23 +28,26 @@ pub async fn get( Path(guild_uuid): Path, Extension(CurrentUser(uuid)): Extension>, ) -> Result { - global_checks(&app_state, uuid).await?; + let mut conn = app_state.pool.get().await?; - Member::check_membership(&mut app_state.pool.get().await?, uuid, guild_uuid).await?; + global_checks(&mut conn, &app_state.config, uuid).await?; + + Member::check_membership(&mut conn, uuid, guild_uuid).await?; if let Ok(cache_hit) = app_state - .get_cache_key(format!("{guild_uuid}_channels")) + .cache_pool + .get_cache_key::>(format!("{guild_uuid}_channels")) .await - && let Ok(channels) = serde_json::from_str::>(&cache_hit) { - return Ok((StatusCode::OK, Json(channels)).into_response()); + return Ok((StatusCode::OK, Json(cache_hit)).into_response()); } - let channels = Channel::fetch_all(&app_state.pool, guild_uuid).await?; + let channels = Channel::fetch_all(&mut conn, guild_uuid).await?; let channels_ordered = order_by_is_above(channels).await?; app_state + .cache_pool .set_cache_key( format!("{guild_uuid}_channels"), channels_ordered.clone(), @@ -61,17 +64,19 @@ pub async fn create( Extension(CurrentUser(uuid)): Extension>, Json(channel_info): Json, ) -> Result { - global_checks(&app_state, uuid).await?; + let mut conn = app_state.pool.get().await?; - let member = - Member::check_membership(&mut app_state.pool.get().await?, uuid, guild_uuid).await?; + global_checks(&mut conn, &app_state.config, uuid).await?; + + let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; member - .check_permission(&app_state, Permissions::ManageChannel) + .check_permission(&mut conn, &app_state.cache_pool, Permissions::ManageChannel) .await?; let channel = Channel::new( - &app_state, + &mut conn, + &app_state.cache_pool, guild_uuid, channel_info.name.clone(), channel_info.description.clone(), diff --git a/src/api/v1/guilds/uuid/invites/mod.rs b/src/api/v1/guilds/uuid/invites/mod.rs index 649fc16..fa06f44 100644 --- a/src/api/v1/guilds/uuid/invites/mod.rs +++ b/src/api/v1/guilds/uuid/invites/mod.rs @@ -27,10 +27,10 @@ pub async fn get( Path(guild_uuid): Path, Extension(CurrentUser(uuid)): Extension>, ) -> Result { - global_checks(&app_state, uuid).await?; - let mut conn = app_state.pool.get().await?; + global_checks(&mut conn, &app_state.config, uuid).await?; + Member::check_membership(&mut conn, uuid, guild_uuid).await?; let guild = Guild::fetch_one(&mut conn, guild_uuid).await?; @@ -46,14 +46,14 @@ pub async fn create( Extension(CurrentUser(uuid)): Extension>, Json(invite_request): Json, ) -> Result { - global_checks(&app_state, uuid).await?; - let mut conn = app_state.pool.get().await?; + global_checks(&mut conn, &app_state.config, uuid).await?; + let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; member - .check_permission(&app_state, Permissions::CreateInvite) + .check_permission(&mut conn, &app_state.cache_pool, Permissions::CreateInvite) .await?; let guild = Guild::fetch_one(&mut conn, guild_uuid).await?; diff --git a/src/api/v1/guilds/uuid/members.rs b/src/api/v1/guilds/uuid/members.rs index 3ae10f7..56710af 100644 --- a/src/api/v1/guilds/uuid/members.rs +++ b/src/api/v1/guilds/uuid/members.rs @@ -21,15 +21,15 @@ pub async fn get( Path(guild_uuid): Path, Extension(CurrentUser(uuid)): Extension>, ) -> Result { - global_checks(&app_state, uuid).await?; - let mut conn = app_state.pool.get().await?; + global_checks(&mut conn, &app_state.config, uuid).await?; + Member::check_membership(&mut conn, uuid, guild_uuid).await?; let me = Me::get(&mut conn, uuid).await?; - let members = Member::fetch_all(&app_state, &me, guild_uuid).await?; + let members = Member::fetch_all(&mut conn, &app_state.cache_pool, &me, guild_uuid).await?; Ok((StatusCode::OK, Json(members))) } diff --git a/src/api/v1/guilds/uuid/mod.rs b/src/api/v1/guilds/uuid/mod.rs index 52f0b64..d49e56a 100644 --- a/src/api/v1/guilds/uuid/mod.rs +++ b/src/api/v1/guilds/uuid/mod.rs @@ -82,10 +82,10 @@ pub async fn get_guild( Path(guild_uuid): Path, Extension(CurrentUser(uuid)): Extension>, ) -> Result { - global_checks(&app_state, uuid).await?; - let mut conn = app_state.pool.get().await?; + global_checks(&mut conn, &app_state.config, uuid).await?; + Member::check_membership(&mut conn, uuid, guild_uuid).await?; let guild = Guild::fetch_one(&mut conn, guild_uuid).await?; @@ -102,14 +102,14 @@ pub async fn edit( Extension(CurrentUser(uuid)): Extension>, mut multipart: Multipart, ) -> Result { - global_checks(&app_state, uuid).await?; - let mut conn = app_state.pool.get().await?; + global_checks(&mut conn, &app_state.config, uuid).await?; + let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; member - .check_permission(&app_state, Permissions::ManageGuild) + .check_permission(&mut conn, &app_state.cache_pool, Permissions::ManageGuild) .await?; let mut guild = Guild::fetch_one(&mut conn, guild_uuid).await?; @@ -127,14 +127,7 @@ pub async fn edit( } if let Some(icon) = icon { - guild - .set_icon( - &app_state.bunny_storage, - &mut conn, - app_state.config.bunny.cdn_url.clone(), - icon, - ) - .await?; + guild.set_icon(&mut conn, &app_state, icon).await?; } Ok(StatusCode::OK) diff --git a/src/api/v1/guilds/uuid/roles/mod.rs b/src/api/v1/guilds/uuid/roles/mod.rs index 0e496a0..d3660ce 100644 --- a/src/api/v1/guilds/uuid/roles/mod.rs +++ b/src/api/v1/guilds/uuid/roles/mod.rs @@ -14,7 +14,7 @@ use crate::{ api::v1::auth::CurrentUser, error::Error, objects::{Member, Permissions, Role}, - utils::{global_checks, order_by_is_above}, + utils::{CacheFns, global_checks, order_by_is_above}, }; pub mod uuid; @@ -29,16 +29,18 @@ pub async fn get( Path(guild_uuid): Path, Extension(CurrentUser(uuid)): Extension>, ) -> Result { - global_checks(&app_state, uuid).await?; - let mut conn = app_state.pool.get().await?; + global_checks(&mut conn, &app_state.config, uuid).await?; + Member::check_membership(&mut conn, uuid, guild_uuid).await?; - if let Ok(cache_hit) = app_state.get_cache_key(format!("{guild_uuid}_roles")).await - && let Ok(roles) = serde_json::from_str::>(&cache_hit) + if let Ok(cache_hit) = app_state + .cache_pool + .get_cache_key::>(format!("{guild_uuid}_roles")) + .await { - return Ok((StatusCode::OK, Json(roles)).into_response()); + return Ok((StatusCode::OK, Json(cache_hit)).into_response()); } let roles = Role::fetch_all(&mut conn, guild_uuid).await?; @@ -46,6 +48,7 @@ pub async fn get( let roles_ordered = order_by_is_above(roles).await?; app_state + .cache_pool .set_cache_key(format!("{guild_uuid}_roles"), roles_ordered.clone(), 1800) .await?; @@ -58,14 +61,14 @@ pub async fn create( Extension(CurrentUser(uuid)): Extension>, Json(role_info): Json, ) -> Result { - global_checks(&app_state, uuid).await?; - let mut conn = app_state.pool.get().await?; + global_checks(&mut conn, &app_state.config, uuid).await?; + let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; member - .check_permission(&app_state, Permissions::ManageRole) + .check_permission(&mut conn, &app_state.cache_pool, Permissions::ManageRole) .await?; let role = Role::new(&mut conn, guild_uuid, role_info.name.clone()).await?; diff --git a/src/api/v1/guilds/uuid/roles/uuid.rs b/src/api/v1/guilds/uuid/roles/uuid.rs index 732d553..e7890d0 100644 --- a/src/api/v1/guilds/uuid/roles/uuid.rs +++ b/src/api/v1/guilds/uuid/roles/uuid.rs @@ -13,7 +13,7 @@ use crate::{ api::v1::auth::CurrentUser, error::Error, objects::{Member, Role}, - utils::global_checks, + utils::{CacheFns, global_checks}, }; pub async fn get( @@ -21,21 +21,24 @@ pub async fn get( Path((guild_uuid, role_uuid)): Path<(Uuid, Uuid)>, Extension(CurrentUser(uuid)): Extension>, ) -> Result { - global_checks(&app_state, uuid).await?; - let mut conn = app_state.pool.get().await?; + global_checks(&mut conn, &app_state.config, uuid).await?; + Member::check_membership(&mut conn, uuid, guild_uuid).await?; - if let Ok(cache_hit) = app_state.get_cache_key(format!("{role_uuid}")).await - && let Ok(role) = serde_json::from_str::(&cache_hit) + if let Ok(cache_hit) = app_state + .cache_pool + .get_cache_key::(format!("{role_uuid}")) + .await { - return Ok((StatusCode::OK, Json(role)).into_response()); + return Ok((StatusCode::OK, Json(cache_hit)).into_response()); } let role = Role::fetch_one(&mut conn, role_uuid).await?; app_state + .cache_pool .set_cache_key(format!("{role_uuid}"), role.clone(), 60) .await?; diff --git a/src/api/v1/invites/id.rs b/src/api/v1/invites/id.rs index 72ceea4..99f177f 100644 --- a/src/api/v1/invites/id.rs +++ b/src/api/v1/invites/id.rs @@ -34,15 +34,15 @@ pub async fn join( Path(invite_id): Path, Extension(CurrentUser(uuid)): Extension>, ) -> Result { - global_checks(&app_state, uuid).await?; - let mut conn = app_state.pool.get().await?; + global_checks(&mut conn, &app_state.config, uuid).await?; + let invite = Invite::fetch_one(&mut conn, invite_id).await?; let guild = Guild::fetch_one(&mut conn, invite.guild_uuid).await?; - Member::new(&app_state, uuid, guild.uuid).await?; + Member::new(&mut conn, &app_state.cache_pool, uuid, guild.uuid).await?; Ok((StatusCode::OK, Json(guild))) } diff --git a/src/api/v1/me/friends/mod.rs b/src/api/v1/me/friends/mod.rs index a56f8d4..904a1f5 100644 --- a/src/api/v1/me/friends/mod.rs +++ b/src/api/v1/me/friends/mod.rs @@ -19,11 +19,13 @@ pub async fn get( State(app_state): State>, Extension(CurrentUser(uuid)): Extension>, ) -> Result { - global_checks(&app_state, uuid).await?; + let mut conn = app_state.pool.get().await?; - let me = Me::get(&mut app_state.pool.get().await?, uuid).await?; + global_checks(&mut conn, &app_state.config, uuid).await?; - let friends = me.get_friends(&app_state).await?; + let me = Me::get(&mut conn, uuid).await?; + + let friends = me.get_friends(&mut conn, &app_state.cache_pool).await?; Ok((StatusCode::OK, Json(friends))) } @@ -57,10 +59,10 @@ pub async fn post( Extension(CurrentUser(uuid)): Extension>, Json(user_request): Json, ) -> Result { - global_checks(&app_state, uuid).await?; - let mut conn = app_state.pool.get().await?; + global_checks(&mut conn, &app_state.config, uuid).await?; + let me = Me::get(&mut conn, uuid).await?; let target_uuid = user_uuid_from_username(&mut conn, &user_request.username).await?; diff --git a/src/api/v1/me/friends/uuid.rs b/src/api/v1/me/friends/uuid.rs index 5367435..35f0742 100644 --- a/src/api/v1/me/friends/uuid.rs +++ b/src/api/v1/me/friends/uuid.rs @@ -17,10 +17,10 @@ pub async fn delete( Path(friend_uuid): Path, Extension(CurrentUser(uuid)): Extension>, ) -> Result { - global_checks(&app_state, uuid).await?; - let mut conn = app_state.pool.get().await?; + global_checks(&mut conn, &app_state.config, uuid).await?; + let me = Me::get(&mut conn, uuid).await?; me.remove_friend(&mut conn, friend_uuid).await?; diff --git a/src/api/v1/me/guilds.rs b/src/api/v1/me/guilds.rs index 88dfad9..42d5c21 100644 --- a/src/api/v1/me/guilds.rs +++ b/src/api/v1/me/guilds.rs @@ -58,10 +58,10 @@ pub async fn get( State(app_state): State>, Extension(CurrentUser(uuid)): Extension>, ) -> Result { - global_checks(&app_state, uuid).await?; - let mut conn = app_state.pool.get().await?; + global_checks(&mut conn, &app_state.config, uuid).await?; + let me = Me::get(&mut conn, uuid).await?; let memberships = me.fetch_memberships(&mut conn).await?; diff --git a/src/api/v1/me/mod.rs b/src/api/v1/me/mod.rs index e167d14..86d3d9e 100644 --- a/src/api/v1/me/mod.rs +++ b/src/api/v1/me/mod.rs @@ -73,36 +73,41 @@ pub async fn update( let json = json_raw.unwrap_or_default(); + let mut conn = app_state.pool.get().await?; + if avatar.is_some() || json.username.is_some() || json.display_name.is_some() { - global_checks(&app_state, uuid).await?; + global_checks(&mut conn, &app_state.config, uuid).await?; } - let mut me = Me::get(&mut app_state.pool.get().await?, uuid).await?; + let mut me = Me::get(&mut conn, uuid).await?; if let Some(avatar) = avatar { - me.set_avatar(&app_state, app_state.config.bunny.cdn_url.clone(), avatar) - .await?; + me.set_avatar(&mut conn, &app_state, avatar).await?; } if let Some(username) = &json.username { - me.set_username(&app_state, username.clone()).await?; + me.set_username(&mut conn, &app_state.cache_pool, username.clone()) + .await?; } if let Some(display_name) = &json.display_name { - me.set_display_name(&app_state, display_name.clone()) + me.set_display_name(&mut conn, &app_state.cache_pool, display_name.clone()) .await?; } if let Some(email) = &json.email { - me.set_email(&app_state, email.clone()).await?; + me.set_email(&mut conn, &app_state.cache_pool, email.clone()) + .await?; } if let Some(pronouns) = &json.pronouns { - me.set_pronouns(&app_state, pronouns.clone()).await?; + me.set_pronouns(&mut conn, &app_state.cache_pool, pronouns.clone()) + .await?; } if let Some(about) = &json.about { - me.set_about(&app_state, about.clone()).await?; + me.set_about(&mut conn, &app_state.cache_pool, about.clone()) + .await?; } Ok(StatusCode::OK) diff --git a/src/api/v1/users/mod.rs b/src/api/v1/users/mod.rs index a4b93ce..999e13f 100644 --- a/src/api/v1/users/mod.rs +++ b/src/api/v1/users/mod.rs @@ -70,9 +70,11 @@ pub async fn users( return Ok(StatusCode::BAD_REQUEST.into_response()); } - global_checks(&app_state, uuid).await?; + let mut conn = app_state.pool.get().await?; - let users = User::fetch_amount(&mut app_state.pool.get().await?, start, amount).await?; + global_checks(&mut conn, &app_state.config, uuid).await?; + + let users = User::fetch_amount(&mut conn, start, amount).await?; Ok((StatusCode::OK, Json(users)).into_response()) } diff --git a/src/api/v1/users/uuid.rs b/src/api/v1/users/uuid.rs index cee6df0..e015c3c 100644 --- a/src/api/v1/users/uuid.rs +++ b/src/api/v1/users/uuid.rs @@ -39,11 +39,14 @@ pub async fn get( Path(user_uuid): Path, Extension(CurrentUser(uuid)): Extension>, ) -> Result { - global_checks(&app_state, uuid).await?; + let mut conn = app_state.pool.get().await?; - let me = Me::get(&mut app_state.pool.get().await?, uuid).await?; + global_checks(&mut conn, &app_state.config, uuid).await?; - let user = User::fetch_one_with_friendship(&app_state, &me, user_uuid).await?; + let me = Me::get(&mut conn, uuid).await?; + + let user = + User::fetch_one_with_friendship(&mut conn, &app_state.cache_pool, &me, user_uuid).await?; Ok((StatusCode::OK, Json(user))) } diff --git a/src/objects/channel.rs b/src/objects/channel.rs index cacb153..03a2cf6 100644 --- a/src/objects/channel.rs +++ b/src/objects/channel.rs @@ -2,15 +2,15 @@ use diesel::{ ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, delete, insert_into, update, }; -use diesel_async::{RunQueryDsl, pooled_connection::AsyncDieselConnectionManager}; +use diesel_async::RunQueryDsl; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ - AppState, Conn, + Conn, error::Error, schema::{channel_permissions, channels, messages}, - utils::{CHANNEL_REGEX, order_by_is_above}, + utils::{CHANNEL_REGEX, CacheFns, order_by_is_above}, }; use super::{HasIsAbove, HasUuid, Message, load_or_empty, message::MessageBuilder}; @@ -79,49 +79,44 @@ impl HasIsAbove for Channel { } impl Channel { - pub async fn fetch_all( - pool: &deadpool::managed::Pool< - AsyncDieselConnectionManager, - Conn, - >, - guild_uuid: Uuid, - ) -> Result, Error> { - let mut conn = pool.get().await?; - + pub async fn fetch_all(conn: &mut Conn, guild_uuid: Uuid) -> Result, Error> { use channels::dsl; let channel_builders: Vec = load_or_empty( dsl::channels .filter(dsl::guild_uuid.eq(guild_uuid)) .select(ChannelBuilder::as_select()) - .load(&mut conn) + .load(conn) .await, )?; - let channel_futures = channel_builders.iter().map(async move |c| { - let mut conn = pool.get().await?; - c.clone().build(&mut conn).await - }); + let mut channels = vec![]; - futures_util::future::try_join_all(channel_futures).await - } - - pub async fn fetch_one(app_state: &AppState, channel_uuid: Uuid) -> Result { - if let Ok(cache_hit) = app_state.get_cache_key(channel_uuid.to_string()).await { - return Ok(serde_json::from_str(&cache_hit)?); + for builder in channel_builders { + channels.push(builder.build(conn).await?); } - let mut conn = app_state.pool.get().await?; + Ok(channels) + } + + pub async fn fetch_one( + conn: &mut Conn, + cache_pool: &redis::Client, + channel_uuid: Uuid, + ) -> Result { + if let Ok(cache_hit) = cache_pool.get_cache_key(channel_uuid.to_string()).await { + return Ok(cache_hit); + } use channels::dsl; let channel_builder: ChannelBuilder = dsl::channels .filter(dsl::uuid.eq(channel_uuid)) .select(ChannelBuilder::as_select()) - .get_result(&mut conn) + .get_result(conn) .await?; - let channel = channel_builder.build(&mut conn).await?; + let channel = channel_builder.build(conn).await?; - app_state + cache_pool .set_cache_key(channel_uuid.to_string(), channel.clone(), 60) .await?; @@ -129,7 +124,8 @@ impl Channel { } pub async fn new( - app_state: &AppState, + conn: &mut Conn, + cache_pool: &redis::Client, guild_uuid: Uuid, name: String, description: Option, @@ -138,11 +134,9 @@ impl Channel { return Err(Error::BadRequest("Channel name is invalid".to_string())); } - let mut conn = app_state.pool.get().await?; - let channel_uuid = Uuid::now_v7(); - let channels = Self::fetch_all(&app_state.pool, guild_uuid).await?; + let channels = Self::fetch_all(conn, guild_uuid).await?; let channels_ordered = order_by_is_above(channels).await?; @@ -158,7 +152,7 @@ impl Channel { insert_into(channels::table) .values(new_channel.clone()) - .execute(&mut conn) + .execute(conn) .await?; if let Some(old_last_channel) = last_channel { @@ -166,7 +160,7 @@ impl Channel { update(channels::table) .filter(dsl::uuid.eq(old_last_channel.uuid)) .set(dsl::is_above.eq(new_channel.uuid)) - .execute(&mut conn) + .execute(conn) .await?; } @@ -180,16 +174,16 @@ impl Channel { permissions: vec![], }; - app_state + cache_pool .set_cache_key(channel_uuid.to_string(), channel.clone(), 1800) .await?; - if app_state - .get_cache_key(format!("{guild_uuid}_channels")) + if cache_pool + .get_cache_key::>(format!("{guild_uuid}_channels")) .await .is_ok() { - app_state + cache_pool .del_cache_key(format!("{guild_uuid}_channels")) .await?; } @@ -197,14 +191,12 @@ impl Channel { Ok(channel) } - pub async fn delete(self, app_state: &AppState) -> Result<(), Error> { - let mut conn = app_state.pool.get().await?; - + pub async fn delete(self, conn: &mut Conn, cache_pool: &redis::Client) -> Result<(), Error> { use channels::dsl; match update(channels::table) .filter(dsl::is_above.eq(self.uuid)) .set(dsl::is_above.eq(None::)) - .execute(&mut conn) + .execute(conn) .await { Ok(r) => Ok(r), @@ -214,13 +206,13 @@ impl Channel { delete(channels::table) .filter(dsl::uuid.eq(self.uuid)) - .execute(&mut conn) + .execute(conn) .await?; match update(channels::table) .filter(dsl::is_above.eq(self.uuid)) .set(dsl::is_above.eq(self.is_above)) - .execute(&mut conn) + .execute(conn) .await { Ok(r) => Ok(r), @@ -228,16 +220,20 @@ impl Channel { Err(e) => Err(e), }?; - if app_state.get_cache_key(self.uuid.to_string()).await.is_ok() { - app_state.del_cache_key(self.uuid.to_string()).await?; - } - - if app_state - .get_cache_key(format!("{}_channels", self.guild_uuid)) + if cache_pool + .get_cache_key::(self.uuid.to_string()) .await .is_ok() { - app_state + cache_pool.del_cache_key(self.uuid.to_string()).await?; + } + + if cache_pool + .get_cache_key::>(format!("{}_channels", self.guild_uuid)) + .await + .is_ok() + { + cache_pool .del_cache_key(format!("{}_channels", self.guild_uuid)) .await?; } @@ -247,32 +243,36 @@ impl Channel { pub async fn fetch_messages( &self, - app_state: &AppState, + conn: &mut Conn, + cache_pool: &redis::Client, amount: i64, offset: i64, ) -> Result, Error> { - let mut conn = app_state.pool.get().await?; - use messages::dsl; - let messages: Vec = load_or_empty( + let message_builders: Vec = load_or_empty( dsl::messages .filter(dsl::channel_uuid.eq(self.uuid)) .select(MessageBuilder::as_select()) .order(dsl::uuid.desc()) .limit(amount) .offset(offset) - .load(&mut conn) + .load(conn) .await, )?; - let message_futures = messages.iter().map(async move |b| b.build(app_state).await); + let mut messages = vec![]; - futures_util::future::try_join_all(message_futures).await + for builder in message_builders { + messages.push(builder.build(conn, cache_pool).await?); + } + + Ok(messages) } pub async fn new_message( &self, - app_state: &AppState, + conn: &mut Conn, + cache_pool: &redis::Client, user_uuid: Uuid, message: String, reply_to: Option, @@ -287,66 +287,101 @@ impl Channel { reply_to, }; - let mut conn = app_state.pool.get().await?; - insert_into(messages::table) .values(message.clone()) - .execute(&mut conn) + .execute(conn) .await?; - message.build(app_state).await + message.build(conn, cache_pool).await } - pub async fn set_name(&mut self, app_state: &AppState, new_name: String) -> Result<(), Error> { + pub async fn set_name( + &mut self, + conn: &mut Conn, + cache_pool: &redis::Client, + new_name: String, + ) -> Result<(), Error> { if !CHANNEL_REGEX.is_match(&new_name) { return Err(Error::BadRequest("Channel name is invalid".to_string())); } - let mut conn = app_state.pool.get().await?; - use channels::dsl; update(channels::table) .filter(dsl::uuid.eq(self.uuid)) .set(dsl::name.eq(&new_name)) - .execute(&mut conn) + .execute(conn) .await?; self.name = new_name; + if cache_pool + .get_cache_key::(self.uuid.to_string()) + .await + .is_ok() + { + cache_pool.del_cache_key(self.uuid.to_string()).await?; + } + + if cache_pool + .get_cache_key::>(format!("{}_channels", self.guild_uuid)) + .await + .is_ok() + { + cache_pool + .del_cache_key(format!("{}_channels", self.guild_uuid)) + .await?; + } + Ok(()) } pub async fn set_description( &mut self, - app_state: &AppState, + conn: &mut Conn, + cache_pool: &redis::Client, new_description: String, ) -> Result<(), Error> { - let mut conn = app_state.pool.get().await?; - use channels::dsl; update(channels::table) .filter(dsl::uuid.eq(self.uuid)) .set(dsl::description.eq(&new_description)) - .execute(&mut conn) + .execute(conn) .await?; self.description = Some(new_description); + if cache_pool + .get_cache_key::(self.uuid.to_string()) + .await + .is_ok() + { + cache_pool.del_cache_key(self.uuid.to_string()).await?; + } + + if cache_pool + .get_cache_key::>(format!("{}_channels", self.guild_uuid)) + .await + .is_ok() + { + cache_pool + .del_cache_key(format!("{}_channels", self.guild_uuid)) + .await?; + } + Ok(()) } pub async fn move_channel( &mut self, - app_state: &AppState, + conn: &mut Conn, + cache_pool: &redis::Client, new_is_above: Uuid, ) -> Result<(), Error> { - let mut conn = app_state.pool.get().await?; - use channels::dsl; let old_above_uuid: Option = match dsl::channels .filter(dsl::is_above.eq(self.uuid)) .select(dsl::uuid) - .get_result(&mut conn) + .get_result(conn) .await { Ok(r) => Ok(Some(r)), @@ -358,14 +393,14 @@ impl Channel { update(channels::table) .filter(dsl::uuid.eq(uuid)) .set(dsl::is_above.eq(None::)) - .execute(&mut conn) + .execute(conn) .await?; } match update(channels::table) .filter(dsl::is_above.eq(new_is_above)) .set(dsl::is_above.eq(self.uuid)) - .execute(&mut conn) + .execute(conn) .await { Ok(r) => Ok(r), @@ -376,19 +411,37 @@ impl Channel { update(channels::table) .filter(dsl::uuid.eq(self.uuid)) .set(dsl::is_above.eq(new_is_above)) - .execute(&mut conn) + .execute(conn) .await?; if let Some(uuid) = old_above_uuid { update(channels::table) .filter(dsl::uuid.eq(uuid)) .set(dsl::is_above.eq(self.is_above)) - .execute(&mut conn) + .execute(conn) .await?; } self.is_above = Some(new_is_above); + if cache_pool + .get_cache_key::(self.uuid.to_string()) + .await + .is_ok() + { + cache_pool.del_cache_key(self.uuid.to_string()).await?; + } + + if cache_pool + .get_cache_key::>(format!("{}_channels", self.guild_uuid)) + .await + .is_ok() + { + cache_pool + .del_cache_key(format!("{}_channels", self.guild_uuid)) + .await?; + } + Ok(()) } } diff --git a/src/objects/email_token.rs b/src/objects/email_token.rs index 64d2fdb..c826620 100644 --- a/src/objects/email_token.rs +++ b/src/objects/email_token.rs @@ -3,7 +3,11 @@ use lettre::message::MultiPart; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::{AppState, error::Error, utils::generate_token}; +use crate::{ + AppState, + error::Error, + utils::{CacheFns, generate_token}, +}; use super::Me; @@ -15,12 +19,10 @@ pub struct EmailToken { } impl EmailToken { - pub async fn get(app_state: &AppState, user_uuid: Uuid) -> Result { - let email_token = serde_json::from_str( - &app_state - .get_cache_key(format!("{user_uuid}_email_verify")) - .await?, - )?; + pub async fn get(cache_pool: &redis::Client, user_uuid: Uuid) -> Result { + let email_token = cache_pool + .get_cache_key(format!("{user_uuid}_email_verify")) + .await?; Ok(email_token) } @@ -37,6 +39,7 @@ impl EmailToken { }; app_state + .cache_pool .set_cache_key(format!("{}_email_verify", me.uuid), email_token, 86400) .await?; @@ -59,8 +62,8 @@ impl EmailToken { Ok(()) } - pub async fn delete(&self, app_state: &AppState) -> Result<(), Error> { - app_state + pub async fn delete(&self, cache_pool: &redis::Client) -> Result<(), Error> { + cache_pool .del_cache_key(format!("{}_email_verify", self.user_uuid)) .await?; diff --git a/src/objects/guild.rs b/src/objects/guild.rs index 9514e49..9640e28 100644 --- a/src/objects/guild.rs +++ b/src/objects/guild.rs @@ -3,14 +3,14 @@ use diesel::{ ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, insert_into, update, }; -use diesel_async::{RunQueryDsl, pooled_connection::AsyncDieselConnectionManager}; +use diesel_async::RunQueryDsl; use serde::Serialize; use tokio::task; use url::Url; use uuid::Uuid; use crate::{ - Conn, + AppState, Conn, error::Error, schema::{guild_members, guilds, invites}, utils::image_check, @@ -68,16 +68,11 @@ impl Guild { } pub async fn fetch_amount( - pool: &deadpool::managed::Pool< - AsyncDieselConnectionManager, - Conn, - >, + conn: &mut Conn, offset: i64, amount: i64, ) -> Result, Error> { // Fetch guild data from database - let mut conn = pool.get().await?; - use guilds::dsl; let guild_builders: Vec = load_or_empty( dsl::guilds @@ -85,18 +80,17 @@ impl Guild { .order_by(dsl::uuid) .offset(offset) .limit(amount) - .load(&mut conn) + .load(conn) .await, )?; - // Process each guild concurrently - let guild_futures = guild_builders.iter().map(async move |g| { - let mut conn = pool.get().await?; - g.clone().build(&mut conn).await - }); + let mut guilds = vec![]; - // Execute all futures concurrently and collect results - futures_util::future::try_join_all(guild_futures).await + for builder in guild_builders { + guilds.push(builder.build(conn).await?); + } + + Ok(guilds) } pub async fn new(conn: &mut Conn, name: String, owner_uuid: Uuid) -> Result { @@ -188,9 +182,8 @@ impl Guild { // FIXME: Horrible security pub async fn set_icon( &mut self, - bunny_storage: &bunny_api_tokio::EdgeStorageClient, conn: &mut Conn, - cdn_url: Url, + app_state: &AppState, icon: Bytes, ) -> Result<(), Error> { let icon_clone = icon.clone(); @@ -199,14 +192,14 @@ impl Guild { if let Some(icon) = &self.icon { let relative_url = icon.path().trim_start_matches('/'); - bunny_storage.delete(relative_url).await?; + app_state.bunny_storage.delete(relative_url).await?; } let path = format!("icons/{}/{}.{}", self.uuid, Uuid::now_v7(), image_type); - bunny_storage.upload(path.clone(), icon).await?; + app_state.bunny_storage.upload(path.clone(), icon).await?; - let icon_url = cdn_url.join(&path)?; + let icon_url = app_state.config.bunny.cdn_url.join(&path)?; use guilds::dsl; update(guilds::table) diff --git a/src/objects/me.rs b/src/objects/me.rs index a0b399d..d03e08b 100644 --- a/src/objects/me.rs +++ b/src/objects/me.rs @@ -14,7 +14,7 @@ use crate::{ error::Error, objects::{Friend, FriendRequest, User}, schema::{friend_requests, friends, guild_members, guilds, users}, - utils::{EMAIL_REGEX, USERNAME_REGEX, image_check}, + utils::{CacheFns, EMAIL_REGEX, USERNAME_REGEX, image_check}, }; use super::{Guild, guild::GuildBuilder, load_or_empty, member::MemberBuilder}; @@ -75,15 +75,13 @@ impl Me { pub async fn set_avatar( &mut self, + conn: &mut Conn, app_state: &AppState, - cdn_url: Url, avatar: Bytes, ) -> Result<(), Error> { let avatar_clone = avatar.clone(); let image_type = task::spawn_blocking(move || image_check(avatar_clone)).await??; - let mut conn = app_state.pool.get().await?; - if let Some(avatar) = &self.avatar { let avatar_url: Url = avatar.parse()?; @@ -96,17 +94,25 @@ impl Me { app_state.bunny_storage.upload(path.clone(), avatar).await?; - let avatar_url = cdn_url.join(&path)?; + let avatar_url = app_state.config.bunny.cdn_url.join(&path)?; use users::dsl; update(users::table) .filter(dsl::uuid.eq(self.uuid)) .set(dsl::avatar.eq(avatar_url.as_str())) - .execute(&mut conn) + .execute(conn) .await?; - if app_state.get_cache_key(self.uuid.to_string()).await.is_ok() { - app_state.del_cache_key(self.uuid.to_string()).await? + if app_state + .cache_pool + .get_cache_key::(self.uuid.to_string()) + .await + .is_ok() + { + app_state + .cache_pool + .del_cache_key(self.uuid.to_string()) + .await? } self.avatar = Some(avatar_url.to_string()); @@ -127,7 +133,8 @@ impl Me { pub async fn set_username( &mut self, - app_state: &AppState, + conn: &mut Conn, + cache_pool: &redis::Client, new_username: String, ) -> Result<(), Error> { if !USERNAME_REGEX.is_match(&new_username) @@ -137,17 +144,19 @@ impl Me { return Err(Error::BadRequest("Invalid username".to_string())); } - let mut conn = app_state.pool.get().await?; - use users::dsl; update(users::table) .filter(dsl::uuid.eq(self.uuid)) .set(dsl::username.eq(new_username.as_str())) - .execute(&mut conn) + .execute(conn) .await?; - if app_state.get_cache_key(self.uuid.to_string()).await.is_ok() { - app_state.del_cache_key(self.uuid.to_string()).await? + if cache_pool + .get_cache_key::(self.uuid.to_string()) + .await + .is_ok() + { + cache_pool.del_cache_key(self.uuid.to_string()).await? } self.username = new_username; @@ -157,11 +166,10 @@ impl Me { pub async fn set_display_name( &mut self, - app_state: &AppState, + conn: &mut Conn, + cache_pool: &redis::Client, new_display_name: String, ) -> Result<(), Error> { - let mut conn = app_state.pool.get().await?; - let new_display_name_option = if new_display_name.is_empty() { None } else { @@ -172,11 +180,15 @@ impl Me { update(users::table) .filter(dsl::uuid.eq(self.uuid)) .set(dsl::display_name.eq(&new_display_name_option)) - .execute(&mut conn) + .execute(conn) .await?; - if app_state.get_cache_key(self.uuid.to_string()).await.is_ok() { - app_state.del_cache_key(self.uuid.to_string()).await? + if cache_pool + .get_cache_key::(self.uuid.to_string()) + .await + .is_ok() + { + cache_pool.del_cache_key(self.uuid.to_string()).await? } self.display_name = new_display_name_option; @@ -186,15 +198,14 @@ impl Me { pub async fn set_email( &mut self, - app_state: &AppState, + conn: &mut Conn, + cache_pool: &redis::Client, new_email: String, ) -> Result<(), Error> { if !EMAIL_REGEX.is_match(&new_email) { return Err(Error::BadRequest("Invalid username".to_string())); } - let mut conn = app_state.pool.get().await?; - use users::dsl; update(users::table) .filter(dsl::uuid.eq(self.uuid)) @@ -202,11 +213,15 @@ impl Me { dsl::email.eq(new_email.as_str()), dsl::email_verified.eq(false), )) - .execute(&mut conn) + .execute(conn) .await?; - if app_state.get_cache_key(self.uuid.to_string()).await.is_ok() { - app_state.del_cache_key(self.uuid.to_string()).await? + if cache_pool + .get_cache_key::(self.uuid.to_string()) + .await + .is_ok() + { + cache_pool.del_cache_key(self.uuid.to_string()).await? } self.email = new_email; @@ -216,20 +231,23 @@ impl Me { pub async fn set_pronouns( &mut self, - app_state: &AppState, + conn: &mut Conn, + cache_pool: &redis::Client, new_pronouns: String, ) -> Result<(), Error> { - let mut conn = app_state.pool.get().await?; - use users::dsl; update(users::table) .filter(dsl::uuid.eq(self.uuid)) .set((dsl::pronouns.eq(new_pronouns.as_str()),)) - .execute(&mut conn) + .execute(conn) .await?; - if app_state.get_cache_key(self.uuid.to_string()).await.is_ok() { - app_state.del_cache_key(self.uuid.to_string()).await? + if cache_pool + .get_cache_key::(self.uuid.to_string()) + .await + .is_ok() + { + cache_pool.del_cache_key(self.uuid.to_string()).await? } Ok(()) @@ -237,20 +255,23 @@ impl Me { pub async fn set_about( &mut self, - app_state: &AppState, + conn: &mut Conn, + cache_pool: &redis::Client, new_about: String, ) -> Result<(), Error> { - let mut conn = app_state.pool.get().await?; - use users::dsl; update(users::table) .filter(dsl::uuid.eq(self.uuid)) .set((dsl::about.eq(new_about.as_str()),)) - .execute(&mut conn) + .execute(conn) .await?; - if app_state.get_cache_key(self.uuid.to_string()).await.is_ok() { - app_state.del_cache_key(self.uuid.to_string()).await? + if cache_pool + .get_cache_key::(self.uuid.to_string()) + .await + .is_ok() + { + cache_pool.del_cache_key(self.uuid.to_string()).await? } Ok(()) @@ -366,16 +387,18 @@ impl Me { Ok(()) } - pub async fn get_friends(&self, app_state: &AppState) -> Result, Error> { + pub async fn get_friends( + &self, + conn: &mut Conn, + cache_pool: &redis::Client, + ) -> Result, Error> { use friends::dsl; - let mut conn = app_state.pool.get().await?; - let friends1 = load_or_empty( dsl::friends .filter(dsl::uuid1.eq(self.uuid)) .select(Friend::as_select()) - .load(&mut conn) + .load(conn) .await, )?; @@ -383,21 +406,21 @@ impl Me { dsl::friends .filter(dsl::uuid2.eq(self.uuid)) .select(Friend::as_select()) - .load(&mut conn) + .load(conn) .await, )?; - let friend_futures = friends1.iter().map(async move |friend| { - User::fetch_one_with_friendship(app_state, self, friend.uuid2).await - }); + let mut friends = vec![]; - let mut friends = futures_util::future::try_join_all(friend_futures).await?; + for friend in friends1 { + friends + .push(User::fetch_one_with_friendship(conn, cache_pool, self, friend.uuid2).await?); + } - let friend_futures = friends2.iter().map(async move |friend| { - User::fetch_one_with_friendship(app_state, self, friend.uuid1).await - }); - - friends.append(&mut futures_util::future::try_join_all(friend_futures).await?); + for friend in friends2 { + friends + .push(User::fetch_one_with_friendship(conn, cache_pool, self, friend.uuid1).await?); + } Ok(friends) } diff --git a/src/objects/member.rs b/src/objects/member.rs index 50b76b0..b7befdc 100644 --- a/src/objects/member.rs +++ b/src/objects/member.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ - AppState, Conn, + Conn, error::Error, objects::{Me, Permissions, Role}, schema::guild_members, @@ -26,13 +26,18 @@ pub struct MemberBuilder { } impl MemberBuilder { - pub async fn build(&self, app_state: &AppState, me: Option<&Me>) -> Result { + pub async fn build( + &self, + conn: &mut Conn, + cache_pool: &redis::Client, + me: Option<&Me>, + ) -> Result { let user; if let Some(me) = me { - user = User::fetch_one_with_friendship(app_state, me, self.user_uuid).await?; + user = User::fetch_one_with_friendship(conn, cache_pool, me, self.user_uuid).await?; } else { - user = User::fetch_one(app_state, self.user_uuid).await?; + user = User::fetch_one(conn, cache_pool, self.user_uuid).await?; } Ok(Member { @@ -47,11 +52,12 @@ impl MemberBuilder { pub async fn check_permission( &self, - app_state: &AppState, + conn: &mut Conn, + cache_pool: &redis::Client, permission: Permissions, ) -> Result<(), Error> { if !self.is_owner { - let roles = Role::fetch_from_member(app_state, self.uuid).await?; + let roles = Role::fetch_from_member(conn, cache_pool, self.uuid).await?; let allowed = roles.iter().any(|r| r.permissions & permission as i64 != 0); if !allowed { return Err(Error::Forbidden("Not allowed".to_string())); @@ -101,56 +107,53 @@ impl Member { } pub async fn fetch_one( - app_state: &AppState, + conn: &mut Conn, + cache_pool: &redis::Client, me: &Me, user_uuid: Uuid, guild_uuid: Uuid, ) -> Result { - let mut conn = app_state.pool.get().await?; - use guild_members::dsl; let member: MemberBuilder = dsl::guild_members .filter(dsl::user_uuid.eq(user_uuid)) .filter(dsl::guild_uuid.eq(guild_uuid)) .select(MemberBuilder::as_select()) - .get_result(&mut conn) + .get_result(conn) .await?; - member.build(app_state, Some(me)).await + member.build(conn, cache_pool, Some(me)).await } pub async fn fetch_all( - app_state: &AppState, + conn: &mut Conn, + cache_pool: &redis::Client, me: &Me, guild_uuid: Uuid, ) -> Result, Error> { - let mut conn = app_state.pool.get().await?; - use guild_members::dsl; let member_builders: Vec = load_or_empty( dsl::guild_members .filter(dsl::guild_uuid.eq(guild_uuid)) .select(MemberBuilder::as_select()) - .load(&mut conn) + .load(conn) .await, )?; let mut members = vec![]; for builder in member_builders { - members.push(builder.build(app_state, Some(me)).await?); + members.push(builder.build(conn, cache_pool, Some(me)).await?); } Ok(members) } pub async fn new( - app_state: &AppState, + conn: &mut Conn, + cache_pool: &redis::Client, user_uuid: Uuid, guild_uuid: Uuid, ) -> Result { - let mut conn = app_state.pool.get().await?; - let member_uuid = Uuid::now_v7(); let member = MemberBuilder { @@ -163,9 +166,9 @@ impl Member { insert_into(guild_members::table) .values(&member) - .execute(&mut conn) + .execute(conn) .await?; - member.build(app_state, None).await + member.build(conn, cache_pool, None).await } } diff --git a/src/objects/message.rs b/src/objects/message.rs index caff969..a5224e0 100644 --- a/src/objects/message.rs +++ b/src/objects/message.rs @@ -2,7 +2,7 @@ use diesel::{Insertable, Queryable, Selectable}; use serde::Serialize; use uuid::Uuid; -use crate::{AppState, error::Error, schema::messages}; +use crate::{Conn, error::Error, schema::messages}; use super::User; @@ -18,8 +18,12 @@ pub struct MessageBuilder { } impl MessageBuilder { - pub async fn build(&self, app_state: &AppState) -> Result { - let user = User::fetch_one(app_state, self.user_uuid).await?; + pub async fn build( + &self, + conn: &mut Conn, + cache_pool: &redis::Client, + ) -> Result { + let user = User::fetch_one(conn, cache_pool, self.user_uuid).await?; Ok(Message { uuid: self.uuid, diff --git a/src/objects/password_reset_token.rs b/src/objects/password_reset_token.rs index 04ff43c..ca5c62f 100644 --- a/src/objects/password_reset_token.rs +++ b/src/objects/password_reset_token.rs @@ -10,10 +10,10 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ - AppState, + AppState, Conn, error::Error, schema::users, - utils::{PASSWORD_REGEX, generate_token, global_checks, user_uuid_from_identifier}, + utils::{CacheFns, PASSWORD_REGEX, generate_token, global_checks, user_uuid_from_identifier}, }; #[derive(Serialize, Deserialize)] @@ -24,50 +24,49 @@ pub struct PasswordResetToken { } impl PasswordResetToken { - pub async fn get(app_state: &AppState, token: String) -> Result { - let user_uuid: Uuid = - serde_json::from_str(&app_state.get_cache_key(token.to_string()).await?)?; - let password_reset_token = serde_json::from_str( - &app_state - .get_cache_key(format!("{user_uuid}_password_reset")) - .await?, - )?; + pub async fn get( + cache_pool: &redis::Client, + token: String, + ) -> Result { + let user_uuid: Uuid = cache_pool.get_cache_key(token.to_string()).await?; + let password_reset_token = cache_pool + .get_cache_key(format!("{user_uuid}_password_reset")) + .await?; Ok(password_reset_token) } pub async fn get_with_identifier( - app_state: &AppState, + conn: &mut Conn, + cache_pool: &redis::Client, identifier: String, ) -> Result { - let mut conn = app_state.pool.get().await?; + let user_uuid = user_uuid_from_identifier(conn, &identifier).await?; - let user_uuid = user_uuid_from_identifier(&mut conn, &identifier).await?; - - let password_reset_token = serde_json::from_str( - &app_state - .get_cache_key(format!("{user_uuid}_password_reset")) - .await?, - )?; + let password_reset_token = cache_pool + .get_cache_key(format!("{user_uuid}_password_reset")) + .await?; Ok(password_reset_token) } #[allow(clippy::new_ret_no_self)] - pub async fn new(app_state: &AppState, identifier: String) -> Result<(), Error> { + pub async fn new( + conn: &mut Conn, + app_state: &AppState, + identifier: String, + ) -> Result<(), Error> { let token = generate_token::<32>()?; - let mut conn = app_state.pool.get().await?; + let user_uuid = user_uuid_from_identifier(conn, &identifier).await?; - let user_uuid = user_uuid_from_identifier(&mut conn, &identifier).await?; - - global_checks(app_state, user_uuid).await?; + global_checks(conn, &app_state.config, user_uuid).await?; use users::dsl as udsl; let (username, email_address): (String, String) = udsl::users .filter(udsl::uuid.eq(user_uuid)) .select((udsl::username, udsl::email)) - .get_result(&mut conn) + .get_result(conn) .await?; let password_reset_token = PasswordResetToken { @@ -77,6 +76,7 @@ impl PasswordResetToken { }; app_state + .cache_pool .set_cache_key( format!("{user_uuid}_password_reset"), password_reset_token, @@ -84,6 +84,7 @@ impl PasswordResetToken { ) .await?; app_state + .cache_pool .set_cache_key(token.clone(), user_uuid, 86400) .await?; @@ -106,7 +107,12 @@ impl PasswordResetToken { Ok(()) } - pub async fn set_password(&self, app_state: &AppState, password: String) -> Result<(), Error> { + pub async fn set_password( + &self, + conn: &mut Conn, + app_state: &AppState, + password: String, + ) -> Result<(), Error> { if !PASSWORD_REGEX.is_match(&password) { return Err(Error::BadRequest( "Please provide a valid password".to_string(), @@ -120,19 +126,17 @@ impl PasswordResetToken { .hash_password(password.as_bytes(), &salt) .map_err(|e| Error::PasswordHashError(e.to_string()))?; - let mut conn = app_state.pool.get().await?; - use users::dsl; update(users::table) .filter(dsl::uuid.eq(self.user_uuid)) .set(dsl::password.eq(hashed_password.to_string())) - .execute(&mut conn) + .execute(conn) .await?; let (username, email_address): (String, String) = dsl::users .filter(dsl::uuid.eq(self.user_uuid)) .select((dsl::username, dsl::email)) - .get_result(&mut conn) + .get_result(conn) .await?; let login_page = app_state.config.web.frontend_url.join("login")?; @@ -149,14 +153,14 @@ impl PasswordResetToken { app_state.mail_client.send_mail(email).await?; - self.delete(app_state).await + self.delete(&app_state.cache_pool).await } - pub async fn delete(&self, app_state: &AppState) -> Result<(), Error> { - app_state + pub async fn delete(&self, cache_pool: &redis::Client) -> Result<(), Error> { + cache_pool .del_cache_key(format!("{}_password_reset", &self.user_uuid)) .await?; - app_state.del_cache_key(self.token.to_string()).await?; + cache_pool.del_cache_key(self.token.to_string()).await?; Ok(()) } diff --git a/src/objects/role.rs b/src/objects/role.rs index ea70686..01e4738 100644 --- a/src/objects/role.rs +++ b/src/objects/role.rs @@ -7,10 +7,10 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ - AppState, Conn, + Conn, error::Error, schema::{role_members, roles}, - utils::order_by_is_above, + utils::{CacheFns, order_by_is_above}, }; use super::{HasIsAbove, HasUuid, load_or_empty}; @@ -75,34 +75,33 @@ impl Role { } pub async fn fetch_from_member( - app_state: &AppState, + conn: &mut Conn, + cache_pool: &redis::Client, member_uuid: Uuid, ) -> Result, Error> { - if let Ok(roles) = app_state + if let Ok(roles) = cache_pool .get_cache_key(format!("{member_uuid}_roles")) .await { - return Ok(serde_json::from_str(&roles)?); + return Ok(roles); } - let mut conn = app_state.pool.get().await?; - use role_members::dsl; let role_memberships: Vec = load_or_empty( dsl::role_members .filter(dsl::member_uuid.eq(member_uuid)) .select(RoleMember::as_select()) - .load(&mut conn) + .load(conn) .await, )?; let mut roles = vec![]; for membership in role_memberships { - roles.push(membership.fetch_role(&mut conn).await?); + roles.push(membership.fetch_role(conn).await?); } - app_state + cache_pool .set_cache_key(format!("{member_uuid}_roles"), roles.clone(), 300) .await?; diff --git a/src/objects/user.rs b/src/objects/user.rs index c1f164d..a686c39 100644 --- a/src/objects/user.rs +++ b/src/objects/user.rs @@ -4,7 +4,7 @@ use diesel_async::RunQueryDsl; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::{AppState, Conn, error::Error, objects::Me, schema::users}; +use crate::{Conn, error::Error, objects::Me, schema::users, utils::CacheFns}; use super::load_or_empty; @@ -46,23 +46,25 @@ pub struct User { } impl User { - pub async fn fetch_one(app_state: &AppState, user_uuid: Uuid) -> Result { - let mut conn = app_state.pool.get().await?; - - if let Ok(cache_hit) = app_state.get_cache_key(user_uuid.to_string()).await { - return Ok(serde_json::from_str(&cache_hit)?); + pub async fn fetch_one( + conn: &mut Conn, + cache_pool: &redis::Client, + user_uuid: Uuid, + ) -> Result { + if let Ok(cache_hit) = cache_pool.get_cache_key(user_uuid.to_string()).await { + return Ok(cache_hit); } use users::dsl; let user_builder: UserBuilder = dsl::users .filter(dsl::uuid.eq(user_uuid)) .select(UserBuilder::as_select()) - .get_result(&mut conn) + .get_result(conn) .await?; let user = user_builder.build(); - app_state + cache_pool .set_cache_key(user_uuid.to_string(), user.clone(), 1800) .await?; @@ -70,15 +72,14 @@ impl User { } pub async fn fetch_one_with_friendship( - app_state: &AppState, + conn: &mut Conn, + cache_pool: &redis::Client, me: &Me, user_uuid: Uuid, ) -> Result { - let mut conn = app_state.pool.get().await?; + let mut user = Self::fetch_one(conn, cache_pool, user_uuid).await?; - let mut user = Self::fetch_one(app_state, user_uuid).await?; - - if let Some(friend) = me.friends_with(&mut conn, user_uuid).await? { + if let Some(friend) = me.friends_with(conn, user_uuid).await? { user.friends_since = Some(friend.accepted_at); } diff --git a/src/utils.rs b/src/utils.rs index e1df906..7ef880e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -8,14 +8,13 @@ use diesel::{ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; use getrandom::fill; use hex::encode; -use redis::RedisError; use regex::Regex; -use serde::Serialize; +use serde::{Serialize, de::DeserializeOwned}; use time::Duration; use uuid::Uuid; use crate::{ - AppState, Conn, + Conn, config::Config, error::Error, objects::{HasIsAbove, HasUuid}, @@ -115,15 +114,13 @@ pub async fn user_uuid_from_username(conn: &mut Conn, username: &String) -> Resu } } -pub async fn global_checks(app_state: &AppState, user_uuid: Uuid) -> Result<(), Error> { - if app_state.config.instance.require_email_verification { - let mut conn = app_state.pool.get().await?; - +pub async fn global_checks(conn: &mut Conn, config: &Config, user_uuid: Uuid) -> Result<(), Error> { + if config.instance.require_email_verification { use users::dsl; let email_verified: bool = dsl::users .filter(dsl::uuid.eq(user_uuid)) .select(dsl::email_verified) - .get_result(&mut conn) + .get_result(conn) .await?; if !email_verified { @@ -161,14 +158,28 @@ where Ok(ordered) } -impl AppState { - pub async fn set_cache_key( +#[allow(async_fn_in_trait)] +pub trait CacheFns { + async fn set_cache_key( + &self, + key: String, + value: impl Serialize, + expire: u32, + ) -> Result<(), Error>; + async fn get_cache_key(&self, key: String) -> Result + where + T: DeserializeOwned; + async fn del_cache_key(&self, key: String) -> Result<(), Error>; +} + +impl CacheFns for redis::Client { + async fn set_cache_key( &self, key: String, value: impl Serialize, expire: u32, ) -> Result<(), Error> { - let mut conn = self.cache_pool.get_multiplexed_tokio_connection().await?; + let mut conn = self.get_multiplexed_tokio_connection().await?; let key_encoded = encode(key); @@ -187,26 +198,31 @@ impl AppState { Ok(()) } - pub async fn get_cache_key(&self, key: String) -> Result { - let mut conn = self.cache_pool.get_multiplexed_tokio_connection().await?; + async fn get_cache_key(&self, key: String) -> Result + where + T: DeserializeOwned, + { + let mut conn = self.get_multiplexed_tokio_connection().await?; let key_encoded = encode(key); - redis::cmd("GET") + let res: String = redis::cmd("GET") .arg(key_encoded) .query_async(&mut conn) - .await + .await?; + + Ok(serde_json::from_str(&res)?) } - pub async fn del_cache_key(&self, key: String) -> Result<(), RedisError> { - let mut conn = self.cache_pool.get_multiplexed_tokio_connection().await?; + async fn del_cache_key(&self, key: String) -> Result<(), Error> { + let mut conn = self.get_multiplexed_tokio_connection().await?; let key_encoded = encode(key); - redis::cmd("DEL") + Ok(redis::cmd("DEL") .arg(key_encoded) .query_async(&mut conn) - .await + .await?) } } From 82f4388dab3182de9d97d4babdafcffd0db099f8 Mon Sep 17 00:00:00 2001 From: BAaboe Date: Tue, 22 Jul 2025 17:29:55 +0200 Subject: [PATCH 113/160] New endpoint 'members' with get and delete --- src/api/v1/members/mod.rs | 22 +++++++++++++ src/api/v1/members/uuid/mod.rs | 56 ++++++++++++++++++++++++++++++++++ src/api/v1/mod.rs | 4 ++- src/objects/member.rs | 28 ++++++++++++++++- 4 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 src/api/v1/members/mod.rs create mode 100644 src/api/v1/members/uuid/mod.rs diff --git a/src/api/v1/members/mod.rs b/src/api/v1/members/mod.rs new file mode 100644 index 0000000..165e533 --- /dev/null +++ b/src/api/v1/members/mod.rs @@ -0,0 +1,22 @@ +use std::sync::Arc; + +use axum::{ + Router, + middleware::from_fn_with_state, + routing::{any, delete, get, patch}, +}; +//use socketioxide::SocketIo; + +use crate::{AppState, api::v1::auth::CurrentUser}; + +mod uuid; + +pub fn router(app_state: Arc) -> Router> { + let router_with_auth = Router::new() + .route("/{uuid}", get(uuid::get)) + .route("/{uuid}", delete(uuid::delete)) + .layer(from_fn_with_state(app_state, CurrentUser::check_auth_layer)); + + Router::new() + .merge(router_with_auth) +} diff --git a/src/api/v1/members/uuid/mod.rs b/src/api/v1/members/uuid/mod.rs new file mode 100644 index 0000000..9000e5a --- /dev/null +++ b/src/api/v1/members/uuid/mod.rs @@ -0,0 +1,56 @@ +//! `/api/v1/channels/{uuid}` Channel specific endpoints + +use std::sync::Arc; + +use crate::{ + AppState, + api::v1::auth::CurrentUser, + error::Error, + objects::{Channel, Member, Permissions, Me}, + utils::global_checks, +}; +use axum::{ + Extension, Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; + +use serde::Deserialize; +use uuid::Uuid; + +pub async fn get( + State(app_state): State>, + Path(member_uuid): Path, + Extension(CurrentUser(uuid)): Extension>, +) -> Result { + global_checks(&app_state, uuid).await?; + + let me = Me::get(&mut app_state.pool.get().await?, uuid).await?; + + let member = Member::fetch_one_with_member(&app_state, &me, member_uuid).await?; + + + Ok((StatusCode::OK, Json(member))) +} + +pub async fn delete( + State(app_state): State>, + Path(member_uuid): Path, + Extension(CurrentUser(uuid)): Extension>, +) -> Result { + global_checks(&app_state, uuid).await?; + + let me = Me::get(&mut app_state.pool.get().await?, uuid).await?; + + let member = Member::fetch_one_with_member(&app_state, &me, member_uuid).await?; + + let deleter = Member::check_membership(&mut app_state.pool.get().await?, uuid, member.guild_uuid).await?; + + deleter.check_permission(&app_state, Permissions::ManageMember).await?; + + member.delete(&mut app_state.pool.get().await?).await?; + + Ok(StatusCode::OK) +} + diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs index 860944c..ee9fbc8 100644 --- a/src/api/v1/mod.rs +++ b/src/api/v1/mod.rs @@ -13,6 +13,7 @@ mod invites; mod me; mod stats; mod users; +mod members; pub fn router(app_state: Arc) -> Router> { let router_with_auth = Router::new() @@ -28,6 +29,7 @@ pub fn router(app_state: Arc) -> Router> { Router::new() .route("/stats", get(stats::res)) .nest("/auth", auth::router(app_state.clone())) - .nest("/channels", channels::router(app_state)) + .nest("/channels", channels::router(app_state.clone())) + .nest("/members", members::router(app_state)) .merge(router_with_auth) } diff --git a/src/objects/member.rs b/src/objects/member.rs index 50b76b0..84ee095 100644 --- a/src/objects/member.rs +++ b/src/objects/member.rs @@ -1,5 +1,5 @@ use diesel::{ - ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, insert_into, + delete, insert_into, ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper }; use diesel_async::RunQueryDsl; use serde::{Deserialize, Serialize}; @@ -119,6 +119,23 @@ impl Member { member.build(app_state, Some(me)).await } + pub async fn fetch_one_with_member ( + app_state: &AppState, + me: &Me, + uuid: Uuid + ) -> Result { + let mut conn = app_state.pool.get().await?; + + use guild_members::dsl; + let member: MemberBuilder = dsl::guild_members + .filter(dsl::uuid.eq(uuid)) + .select(MemberBuilder::as_select()) + .get_result(&mut conn) + .await?; + + member.build(app_state, Some(me)).await + } + pub async fn fetch_all( app_state: &AppState, me: &Me, @@ -168,4 +185,13 @@ impl Member { member.build(app_state, None).await } + + pub async fn delete(self, conn: &mut Conn) -> Result<(), Error> { + delete(guild_members::table) + .filter(guild_members::uuid.eq(self.uuid)) + .execute(conn) + .await?; + + Ok(()) + } } From 4ec36c1cda7e4fdfb7724d5d3439e631b648160b Mon Sep 17 00:00:00 2001 From: BAaboe Date: Tue, 22 Jul 2025 17:35:02 +0200 Subject: [PATCH 114/160] path name fix --- src/api/v1/{members => member}/mod.rs | 1 - src/api/v1/{members => member}/uuid/mod.rs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) rename src/api/v1/{members => member}/mod.rs (94%) rename src/api/v1/{members => member}/uuid/mod.rs (96%) diff --git a/src/api/v1/members/mod.rs b/src/api/v1/member/mod.rs similarity index 94% rename from src/api/v1/members/mod.rs rename to src/api/v1/member/mod.rs index 165e533..3ee9718 100644 --- a/src/api/v1/members/mod.rs +++ b/src/api/v1/member/mod.rs @@ -5,7 +5,6 @@ use axum::{ middleware::from_fn_with_state, routing::{any, delete, get, patch}, }; -//use socketioxide::SocketIo; use crate::{AppState, api::v1::auth::CurrentUser}; diff --git a/src/api/v1/members/uuid/mod.rs b/src/api/v1/member/uuid/mod.rs similarity index 96% rename from src/api/v1/members/uuid/mod.rs rename to src/api/v1/member/uuid/mod.rs index 9000e5a..f0425dd 100644 --- a/src/api/v1/members/uuid/mod.rs +++ b/src/api/v1/member/uuid/mod.rs @@ -1,4 +1,4 @@ -//! `/api/v1/channels/{uuid}` Channel specific endpoints +//! `/api/v1/member/{uuid}` Member specific endpoints use std::sync::Arc; From 228bc68327c1e494fd0fee174891bc2cad4b0b17 Mon Sep 17 00:00:00 2001 From: BAaboe Date: Tue, 22 Jul 2025 17:38:04 +0200 Subject: [PATCH 115/160] more path name fix --- src/api/v1/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs index ee9fbc8..b05774e 100644 --- a/src/api/v1/mod.rs +++ b/src/api/v1/mod.rs @@ -13,7 +13,7 @@ mod invites; mod me; mod stats; mod users; -mod members; +mod member; pub fn router(app_state: Arc) -> Router> { let router_with_auth = Router::new() @@ -30,6 +30,6 @@ pub fn router(app_state: Arc) -> Router> { .route("/stats", get(stats::res)) .nest("/auth", auth::router(app_state.clone())) .nest("/channels", channels::router(app_state.clone())) - .nest("/members", members::router(app_state)) + .nest("/member", member::router(app_state)) .merge(router_with_auth) } From 0468d1adca6d2011fb538219263b8b22b2deaeaa Mon Sep 17 00:00:00 2001 From: BAaboe Date: Tue, 22 Jul 2025 18:05:19 +0200 Subject: [PATCH 116/160] fix: Unecessary merge of routers --- src/api/v1/member/mod.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/api/v1/member/mod.rs b/src/api/v1/member/mod.rs index 3ee9718..61dc085 100644 --- a/src/api/v1/member/mod.rs +++ b/src/api/v1/member/mod.rs @@ -11,11 +11,8 @@ use crate::{AppState, api::v1::auth::CurrentUser}; mod uuid; pub fn router(app_state: Arc) -> Router> { - let router_with_auth = Router::new() + Router::new() .route("/{uuid}", get(uuid::get)) .route("/{uuid}", delete(uuid::delete)) - .layer(from_fn_with_state(app_state, CurrentUser::check_auth_layer)); - - Router::new() - .merge(router_with_auth) + .layer(from_fn_with_state(app_state, CurrentUser::check_auth_layer)) } From ea33230e58157fd7cb575a330b7218b84fda1752 Mon Sep 17 00:00:00 2001 From: BAaboe Date: Tue, 22 Jul 2025 18:10:37 +0200 Subject: [PATCH 117/160] fix: reduced numder of function calls to get conn --- src/api/v1/member/uuid/mod.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/api/v1/member/uuid/mod.rs b/src/api/v1/member/uuid/mod.rs index f0425dd..16cc7a9 100644 --- a/src/api/v1/member/uuid/mod.rs +++ b/src/api/v1/member/uuid/mod.rs @@ -1,4 +1,4 @@ -//! `/api/v1/member/{uuid}` Member specific endpoints +//! `/api/v1/members/{uuid}` Member specific endpoints use std::sync::Arc; @@ -41,15 +41,17 @@ pub async fn delete( ) -> Result { global_checks(&app_state, uuid).await?; - let me = Me::get(&mut app_state.pool.get().await?, uuid).await?; + let mut conn = app_state.pool.get().await?; + + let me = Me::get(&mut conn, uuid).await?; let member = Member::fetch_one_with_member(&app_state, &me, member_uuid).await?; - let deleter = Member::check_membership(&mut app_state.pool.get().await?, uuid, member.guild_uuid).await?; + let deleter = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; deleter.check_permission(&app_state, Permissions::ManageMember).await?; - member.delete(&mut app_state.pool.get().await?).await?; + member.delete(&mut conn).await?; Ok(StatusCode::OK) } From 31596c6bfe49532620688b63c8fc1628548d7cee Mon Sep 17 00:00:00 2001 From: BAaboe Date: Tue, 22 Jul 2025 18:26:04 +0200 Subject: [PATCH 118/160] fix: memebrs not member as endpoint --- src/api/v1/{member => members}/mod.rs | 3 +-- src/api/v1/{member => members}/uuid/mod.rs | 0 src/api/v1/mod.rs | 6 +++--- 3 files changed, 4 insertions(+), 5 deletions(-) rename src/api/v1/{member => members}/mod.rs (67%) rename src/api/v1/{member => members}/uuid/mod.rs (100%) diff --git a/src/api/v1/member/mod.rs b/src/api/v1/members/mod.rs similarity index 67% rename from src/api/v1/member/mod.rs rename to src/api/v1/members/mod.rs index 61dc085..9e1ea52 100644 --- a/src/api/v1/member/mod.rs +++ b/src/api/v1/members/mod.rs @@ -10,9 +10,8 @@ use crate::{AppState, api::v1::auth::CurrentUser}; mod uuid; -pub fn router(app_state: Arc) -> Router> { +pub fn router() -> Router> { Router::new() .route("/{uuid}", get(uuid::get)) .route("/{uuid}", delete(uuid::delete)) - .layer(from_fn_with_state(app_state, CurrentUser::check_auth_layer)) } diff --git a/src/api/v1/member/uuid/mod.rs b/src/api/v1/members/uuid/mod.rs similarity index 100% rename from src/api/v1/member/uuid/mod.rs rename to src/api/v1/members/uuid/mod.rs diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs index b05774e..b2c9c99 100644 --- a/src/api/v1/mod.rs +++ b/src/api/v1/mod.rs @@ -13,7 +13,7 @@ mod invites; mod me; mod stats; mod users; -mod member; +mod members; pub fn router(app_state: Arc) -> Router> { let router_with_auth = Router::new() @@ -29,7 +29,7 @@ pub fn router(app_state: Arc) -> Router> { Router::new() .route("/stats", get(stats::res)) .nest("/auth", auth::router(app_state.clone())) - .nest("/channels", channels::router(app_state.clone())) - .nest("/member", member::router(app_state)) + .nest("/channels", channels::router(app_state)) + .nest("/members", members::router()) .merge(router_with_auth) } From 6dd8ddb0df8c3c5ff67af1d584734a71f64c75a6 Mon Sep 17 00:00:00 2001 From: BAaboe Date: Tue, 22 Jul 2025 18:27:36 +0200 Subject: [PATCH 119/160] fix: members in router_with_auth --- src/api/v1/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs index b2c9c99..3aee3a0 100644 --- a/src/api/v1/mod.rs +++ b/src/api/v1/mod.rs @@ -20,6 +20,7 @@ pub fn router(app_state: Arc) -> Router> { .nest("/users", users::router()) .nest("/guilds", guilds::router()) .nest("/invites", invites::router()) + .nest("/members", members::router()) .nest("/me", me::router()) .layer(from_fn_with_state( app_state.clone(), @@ -30,6 +31,5 @@ pub fn router(app_state: Arc) -> Router> { .route("/stats", get(stats::res)) .nest("/auth", auth::router(app_state.clone())) .nest("/channels", channels::router(app_state)) - .nest("/members", members::router()) .merge(router_with_auth) } From a3c460a611f43bf3bf4d5a2ad7ebfb78f6d40d44 Mon Sep 17 00:00:00 2001 From: BAaboe Date: Tue, 22 Jul 2025 18:33:41 +0200 Subject: [PATCH 120/160] fix: Only people in a server should see its members list --- src/api/v1/members/uuid/mod.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/api/v1/members/uuid/mod.rs b/src/api/v1/members/uuid/mod.rs index 16cc7a9..48f22e5 100644 --- a/src/api/v1/members/uuid/mod.rs +++ b/src/api/v1/members/uuid/mod.rs @@ -26,9 +26,12 @@ pub async fn get( ) -> Result { global_checks(&app_state, uuid).await?; - let me = Me::get(&mut app_state.pool.get().await?, uuid).await?; + let mut conn = app_state.pool.get().await?; + + let me = Me::get(&mut conn, uuid).await?; let member = Member::fetch_one_with_member(&app_state, &me, member_uuid).await?; + Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; Ok((StatusCode::OK, Json(member))) From c26ec49e057e472e209747a63d4c2b653d41db5b Mon Sep 17 00:00:00 2001 From: BAaboe Date: Tue, 22 Jul 2025 18:50:17 +0200 Subject: [PATCH 121/160] fix: cargo clippy --fix && cargo fmt --- src/api/v1/members/mod.rs | 5 ++--- src/api/v1/members/uuid/mod.rs | 11 +++++------ src/api/v1/mod.rs | 2 +- src/objects/member.rs | 9 +++++---- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/api/v1/members/mod.rs b/src/api/v1/members/mod.rs index 9e1ea52..34b6938 100644 --- a/src/api/v1/members/mod.rs +++ b/src/api/v1/members/mod.rs @@ -2,11 +2,10 @@ use std::sync::Arc; use axum::{ Router, - middleware::from_fn_with_state, - routing::{any, delete, get, patch}, + routing::{delete, get}, }; -use crate::{AppState, api::v1::auth::CurrentUser}; +use crate::AppState; mod uuid; diff --git a/src/api/v1/members/uuid/mod.rs b/src/api/v1/members/uuid/mod.rs index 48f22e5..244f5f8 100644 --- a/src/api/v1/members/uuid/mod.rs +++ b/src/api/v1/members/uuid/mod.rs @@ -6,7 +6,7 @@ use crate::{ AppState, api::v1::auth::CurrentUser, error::Error, - objects::{Channel, Member, Permissions, Me}, + objects::{Me, Member, Permissions}, utils::global_checks, }; use axum::{ @@ -16,7 +16,6 @@ use axum::{ response::IntoResponse, }; -use serde::Deserialize; use uuid::Uuid; pub async fn get( @@ -32,7 +31,6 @@ pub async fn get( let member = Member::fetch_one_with_member(&app_state, &me, member_uuid).await?; Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; - Ok((StatusCode::OK, Json(member))) } @@ -51,11 +49,12 @@ pub async fn delete( let member = Member::fetch_one_with_member(&app_state, &me, member_uuid).await?; let deleter = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; - - deleter.check_permission(&app_state, Permissions::ManageMember).await?; + + deleter + .check_permission(&app_state, Permissions::ManageMember) + .await?; member.delete(&mut conn).await?; Ok(StatusCode::OK) } - diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs index 3aee3a0..70271ef 100644 --- a/src/api/v1/mod.rs +++ b/src/api/v1/mod.rs @@ -11,9 +11,9 @@ mod channels; mod guilds; mod invites; mod me; +mod members; mod stats; mod users; -mod members; pub fn router(app_state: Arc) -> Router> { let router_with_auth = Router::new() diff --git a/src/objects/member.rs b/src/objects/member.rs index 84ee095..8678f4a 100644 --- a/src/objects/member.rs +++ b/src/objects/member.rs @@ -1,5 +1,6 @@ use diesel::{ - delete, insert_into, ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper + ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, delete, + insert_into, }; use diesel_async::RunQueryDsl; use serde::{Deserialize, Serialize}; @@ -119,10 +120,10 @@ impl Member { member.build(app_state, Some(me)).await } - pub async fn fetch_one_with_member ( + pub async fn fetch_one_with_member( app_state: &AppState, me: &Me, - uuid: Uuid + uuid: Uuid, ) -> Result { let mut conn = app_state.pool.get().await?; @@ -191,7 +192,7 @@ impl Member { .filter(guild_members::uuid.eq(self.uuid)) .execute(conn) .await?; - + Ok(()) } } From 45978bb41af9a42197dcedc2d6a8284dc7694b8a Mon Sep 17 00:00:00 2001 From: Radical Date: Tue, 22 Jul 2025 18:55:26 +0200 Subject: [PATCH 122/160] ci: only run on push prevents duplicate CIs from running at the same time --- .woodpecker/build-and-publish.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.woodpecker/build-and-publish.yml b/.woodpecker/build-and-publish.yml index 66f377d..7f7096b 100644 --- a/.woodpecker/build-and-publish.yml +++ b/.woodpecker/build-and-publish.yml @@ -5,7 +5,6 @@ steps: - cargo build --release when: - event: push - - event: pull_request - name: build-arm64 image: rust:1.88-bookworm @@ -20,7 +19,6 @@ steps: PKG_CONFIG_PATH: /usr/aarch64-linux-gnu/lib/pkgconfig when: - event: push - - event: pull_request - name: container-build-and-publish image: docker From b2e6d3f5530cea8458374957e224a49d178e0ae9 Mon Sep 17 00:00:00 2001 From: BAaboe Date: Tue, 22 Jul 2025 21:58:14 +0200 Subject: [PATCH 123/160] feat: added ban table to the database --- migrations/2025-07-22-195121_add_ban/down.sql | 2 ++ migrations/2025-07-22-195121_add_ban/up.sql | 7 +++++++ 2 files changed, 9 insertions(+) create mode 100644 migrations/2025-07-22-195121_add_ban/down.sql create mode 100644 migrations/2025-07-22-195121_add_ban/up.sql diff --git a/migrations/2025-07-22-195121_add_ban/down.sql b/migrations/2025-07-22-195121_add_ban/down.sql new file mode 100644 index 0000000..62fe554 --- /dev/null +++ b/migrations/2025-07-22-195121_add_ban/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE guild_bans; diff --git a/migrations/2025-07-22-195121_add_ban/up.sql b/migrations/2025-07-22-195121_add_ban/up.sql new file mode 100644 index 0000000..3dd5f9b --- /dev/null +++ b/migrations/2025-07-22-195121_add_ban/up.sql @@ -0,0 +1,7 @@ +-- Your SQL goes here +CREATE TABLE guild_bans ( + guild_ban uuid NOT NULL REFERENCES guilds(uuid) ON DELETE CASCADE, + user_ban uuid NOT NULL REFERENCES users(uuid), + reason VARCHAR(200) DEFAULT NULL, + PRIMARY KEY (user_uuid, guild_uuid) +); From af7193750686de6d3ebfcb7cbe3f091c19008671 Mon Sep 17 00:00:00 2001 From: BAaboe Date: Tue, 22 Jul 2025 22:01:48 +0200 Subject: [PATCH 124/160] fix: fixed the ban table names --- migrations/2025-07-22-195121_add_ban/up.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/migrations/2025-07-22-195121_add_ban/up.sql b/migrations/2025-07-22-195121_add_ban/up.sql index 3dd5f9b..020a1b0 100644 --- a/migrations/2025-07-22-195121_add_ban/up.sql +++ b/migrations/2025-07-22-195121_add_ban/up.sql @@ -1,7 +1,7 @@ -- Your SQL goes here CREATE TABLE guild_bans ( - guild_ban uuid NOT NULL REFERENCES guilds(uuid) ON DELETE CASCADE, - user_ban uuid NOT NULL REFERENCES users(uuid), + guild_uuid uuid NOT NULL REFERENCES guilds(uuid) ON DELETE CASCADE, + user_uuid uuid NOT NULL REFERENCES users(uuid), reason VARCHAR(200) DEFAULT NULL, PRIMARY KEY (user_uuid, guild_uuid) ); From ade45780fa72ebbcfa374833aea38b3136f924bc Mon Sep 17 00:00:00 2001 From: BAaboe Date: Tue, 22 Jul 2025 22:04:48 +0200 Subject: [PATCH 125/160] fix: forgot to add the change on schema --- src/schema.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/schema.rs b/src/schema.rs index 4095dcd..54284d6 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -47,6 +47,15 @@ diesel::table! { } } +diesel::table! { + guild_bans (user_uuid, guild_uuid) { + guild_uuid -> Uuid, + user_uuid -> Uuid, + #[max_length = 200] + reason -> Nullable, + } +} + diesel::table! { guild_members (uuid) { uuid -> Uuid, @@ -154,6 +163,8 @@ diesel::joinable!(access_tokens -> refresh_tokens (refresh_token)); diesel::joinable!(access_tokens -> users (uuid)); diesel::joinable!(channel_permissions -> channels (channel_uuid)); diesel::joinable!(channels -> guilds (guild_uuid)); +diesel::joinable!(guild_bans -> guilds (guild_uuid)); +diesel::joinable!(guild_bans -> users (user_uuid)); diesel::joinable!(guild_members -> guilds (guild_uuid)); diesel::joinable!(guild_members -> users (user_uuid)); diesel::joinable!(instance_permissions -> users (uuid)); @@ -171,6 +182,7 @@ diesel::allow_tables_to_appear_in_same_query!( channels, friend_requests, friends, + guild_bans, guild_members, guilds, instance_permissions, From f175c1932536be1063bff8561f73fad5be32c8ee Mon Sep 17 00:00:00 2001 From: BAaboe Date: Tue, 22 Jul 2025 23:18:21 +0200 Subject: [PATCH 126/160] fix: let you pass None to fetch_one_with_member for me --- src/api/v1/members/uuid/mod.rs | 6 ++++-- src/objects/member.rs | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/api/v1/members/uuid/mod.rs b/src/api/v1/members/uuid/mod.rs index 244f5f8..7c39bff 100644 --- a/src/api/v1/members/uuid/mod.rs +++ b/src/api/v1/members/uuid/mod.rs @@ -1,5 +1,7 @@ //! `/api/v1/members/{uuid}` Member specific endpoints +pub mod ban; + use std::sync::Arc; use crate::{ @@ -29,7 +31,7 @@ pub async fn get( let me = Me::get(&mut conn, uuid).await?; - let member = Member::fetch_one_with_member(&app_state, &me, member_uuid).await?; + let member = Member::fetch_one_with_member(&app_state, Some(&me), member_uuid).await?; Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; Ok((StatusCode::OK, Json(member))) @@ -46,7 +48,7 @@ pub async fn delete( let me = Me::get(&mut conn, uuid).await?; - let member = Member::fetch_one_with_member(&app_state, &me, member_uuid).await?; + let member = Member::fetch_one_with_member(&app_state, Some(&me), member_uuid).await?; let deleter = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; diff --git a/src/objects/member.rs b/src/objects/member.rs index 8678f4a..337c2e9 100644 --- a/src/objects/member.rs +++ b/src/objects/member.rs @@ -122,7 +122,7 @@ impl Member { pub async fn fetch_one_with_member( app_state: &AppState, - me: &Me, + me: Option<&Me>, uuid: Uuid, ) -> Result { let mut conn = app_state.pool.get().await?; @@ -134,7 +134,7 @@ impl Member { .get_result(&mut conn) .await?; - member.build(app_state, Some(me)).await + member.build(app_state, me).await } pub async fn fetch_all( From ad24215fef631b1dc1a81e8fc9e3277f6903cf3a Mon Sep 17 00:00:00 2001 From: BAaboe Date: Wed, 23 Jul 2025 00:50:50 +0200 Subject: [PATCH 127/160] feat: added endpoint for banning --- src/api/v1/members/mod.rs | 3 ++- src/api/v1/members/uuid/ban.rs | 47 ++++++++++++++++++++++++++++++++++ src/objects/member.rs | 37 ++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 src/api/v1/members/uuid/ban.rs diff --git a/src/api/v1/members/mod.rs b/src/api/v1/members/mod.rs index 34b6938..59ceac2 100644 --- a/src/api/v1/members/mod.rs +++ b/src/api/v1/members/mod.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use axum::{ Router, - routing::{delete, get}, + routing::{delete, get, post}, }; use crate::AppState; @@ -13,4 +13,5 @@ pub fn router() -> Router> { Router::new() .route("/{uuid}", get(uuid::get)) .route("/{uuid}", delete(uuid::delete)) + .route("/{uuid}/ban", post(uuid::ban::post)) } diff --git a/src/api/v1/members/uuid/ban.rs b/src/api/v1/members/uuid/ban.rs new file mode 100644 index 0000000..3fb1b58 --- /dev/null +++ b/src/api/v1/members/uuid/ban.rs @@ -0,0 +1,47 @@ +use std::sync::Arc; + +use axum::{ + Extension, + extract::{Path, State, Json}, + http::StatusCode, + response::IntoResponse, +}; +use diesel::{insert_into, RunQueryDsl}; +use serde::Deserialize; + +use crate::{ + api::v1::auth::CurrentUser, error::Error, objects::{Me, Member, Permissions}, schema::guild_bans::{self, dsl}, utils::global_checks, AppState +}; + +use uuid::Uuid; + +#[derive(Deserialize)] +pub struct RequstBody { + reason: String +} + + +pub async fn post( + State(app_state): State>, + Path(member_uuid): Path, + Extension(CurrentUser(uuid)): Extension>, + Json(payload): Json, +) -> Result{ + global_checks(&app_state, uuid).await?; + + let mut conn = app_state.pool.get().await?; + + let member = Member::fetch_one_with_member(&app_state, None, member_uuid).await?; + + if member.is_owner { + return Err(Error::Forbidden("Not allowed".to_string())); + } + + let baner = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; + baner.check_permission(&app_state, Permissions::ManageMember).await?; + + member.ban(&mut conn, &payload.reason).await?; + + + Ok(StatusCode::OK) +} diff --git a/src/objects/member.rs b/src/objects/member.rs index 337c2e9..40bcaee 100644 --- a/src/objects/member.rs +++ b/src/objects/member.rs @@ -1,3 +1,4 @@ +use axum::http::StatusCode; use diesel::{ ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, delete, insert_into, @@ -11,6 +12,7 @@ use crate::{ error::Error, objects::{Me, Permissions, Role}, schema::guild_members, + schema::guild_bans, }; use super::{User, load_or_empty}; @@ -73,6 +75,13 @@ pub struct Member { user: User, } +#[derive(Serialize, Deserialize)] +pub struct GuildBan { + pub guild_uuid: Uuid, + pub user_uuid: Uuid, + pub reason: String, +} + impl Member { pub async fn count(conn: &mut Conn, guild_uuid: Uuid) -> Result { use guild_members::dsl; @@ -169,6 +178,18 @@ impl Member { ) -> Result { let mut conn = app_state.pool.get().await?; + use guild_bans::dsl; + let banned = dsl::guild_bans + .filter(guild_bans::guild_uuid.eq(guild_uuid)) + .filter(guild_bans::user_uuid.eq(user_uuid)) + .execute(&mut conn) + .await; + match banned { + Ok(_) => Err(Error::Forbidden("User banned".to_string())), + Err(diesel::result::Error::NotFound) => Ok(()), + Err(e) => Err(e.into()), + }?; + let member_uuid = Uuid::now_v7(); let member = MemberBuilder { @@ -195,4 +216,20 @@ impl Member { Ok(()) } + + pub async fn ban(self, conn: &mut Conn, reason: &String) -> Result<(), Error> { + use guild_bans::dsl; + insert_into(guild_bans::table) + .values(( + dsl::guild_uuid.eq(self.guild_uuid), + dsl::user_uuid.eq(self.user_uuid), + dsl::reason.eq(reason), + )) + .execute(conn) + .await?; + + self.delete(conn).await?; + + Ok(()) + } } From 8e31dc7acab5fd147e53d2d5d0c3782b53ee418a Mon Sep 17 00:00:00 2001 From: BAaboe Date: Wed, 23 Jul 2025 00:52:19 +0200 Subject: [PATCH 128/160] style: cargo clippy --fix && cargo fmt --- src/api/v1/members/uuid/ban.rs | 23 +++++++++++++---------- src/objects/member.rs | 3 +-- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/api/v1/members/uuid/ban.rs b/src/api/v1/members/uuid/ban.rs index 3fb1b58..986b98d 100644 --- a/src/api/v1/members/uuid/ban.rs +++ b/src/api/v1/members/uuid/ban.rs @@ -2,46 +2,49 @@ use std::sync::Arc; use axum::{ Extension, - extract::{Path, State, Json}, + extract::{Json, Path, State}, http::StatusCode, response::IntoResponse, }; -use diesel::{insert_into, RunQueryDsl}; use serde::Deserialize; use crate::{ - api::v1::auth::CurrentUser, error::Error, objects::{Me, Member, Permissions}, schema::guild_bans::{self, dsl}, utils::global_checks, AppState + AppState, + api::v1::auth::CurrentUser, + error::Error, + objects::{Member, Permissions}, + utils::global_checks, }; use uuid::Uuid; #[derive(Deserialize)] pub struct RequstBody { - reason: String + reason: String, } - pub async fn post( State(app_state): State>, Path(member_uuid): Path, Extension(CurrentUser(uuid)): Extension>, Json(payload): Json, -) -> Result{ +) -> Result { global_checks(&app_state, uuid).await?; let mut conn = app_state.pool.get().await?; - + let member = Member::fetch_one_with_member(&app_state, None, member_uuid).await?; if member.is_owner { return Err(Error::Forbidden("Not allowed".to_string())); } - + let baner = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; - baner.check_permission(&app_state, Permissions::ManageMember).await?; + baner + .check_permission(&app_state, Permissions::ManageMember) + .await?; member.ban(&mut conn, &payload.reason).await?; - Ok(StatusCode::OK) } diff --git a/src/objects/member.rs b/src/objects/member.rs index 40bcaee..d07b701 100644 --- a/src/objects/member.rs +++ b/src/objects/member.rs @@ -1,4 +1,3 @@ -use axum::http::StatusCode; use diesel::{ ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, delete, insert_into, @@ -11,8 +10,8 @@ use crate::{ AppState, Conn, error::Error, objects::{Me, Permissions, Role}, - schema::guild_members, schema::guild_bans, + schema::guild_members, }; use super::{User, load_or_empty}; From 71d44f6c21beb62d5a234e2b66b00831bfc16799 Mon Sep 17 00:00:00 2001 From: BAaboe Date: Wed, 23 Jul 2025 00:58:51 +0200 Subject: [PATCH 129/160] fix: Can not kick owner --- src/api/v1/members/uuid/mod.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/api/v1/members/uuid/mod.rs b/src/api/v1/members/uuid/mod.rs index 244f5f8..7f1709b 100644 --- a/src/api/v1/members/uuid/mod.rs +++ b/src/api/v1/members/uuid/mod.rs @@ -48,6 +48,10 @@ pub async fn delete( let member = Member::fetch_one_with_member(&app_state, &me, member_uuid).await?; + if member.is_owner { + return Error::Forbidden("Can not kick owner".to_string()); + } + let deleter = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; deleter From 6999b4120e726425e6ca32bf87d7798eaae1a078 Mon Sep 17 00:00:00 2001 From: BAaboe Date: Wed, 23 Jul 2025 01:00:17 +0200 Subject: [PATCH 130/160] fix: Updated error message when banning owner --- src/api/v1/members/uuid/ban.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/v1/members/uuid/ban.rs b/src/api/v1/members/uuid/ban.rs index 986b98d..bbbf73e 100644 --- a/src/api/v1/members/uuid/ban.rs +++ b/src/api/v1/members/uuid/ban.rs @@ -36,7 +36,7 @@ pub async fn post( let member = Member::fetch_one_with_member(&app_state, None, member_uuid).await?; if member.is_owner { - return Err(Error::Forbidden("Not allowed".to_string())); + return Err(Error::Forbidden("Can not ban owner".to_string())); } let baner = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; From 9e5d2daeab315e25a9860c4e7d80bd87c27b8dd8 Mon Sep 17 00:00:00 2001 From: BAaboe Date: Wed, 23 Jul 2025 01:07:41 +0200 Subject: [PATCH 131/160] fix: fixed error error --- src/api/v1/members/uuid/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/v1/members/uuid/mod.rs b/src/api/v1/members/uuid/mod.rs index 7f1709b..580c586 100644 --- a/src/api/v1/members/uuid/mod.rs +++ b/src/api/v1/members/uuid/mod.rs @@ -49,7 +49,7 @@ pub async fn delete( let member = Member::fetch_one_with_member(&app_state, &me, member_uuid).await?; if member.is_owner { - return Error::Forbidden("Can not kick owner".to_string()); + return Err(Error::Forbidden("Can not kick owner".to_string())); } let deleter = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; From 2bc702f8d2097ecfc1c28f38edf26e2ae53b6080 Mon Sep 17 00:00:00 2001 From: BAaboe Date: Wed, 23 Jul 2025 01:18:08 +0200 Subject: [PATCH 132/160] fix: baner :) --- src/api/v1/members/uuid/ban.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/v1/members/uuid/ban.rs b/src/api/v1/members/uuid/ban.rs index bbbf73e..b543975 100644 --- a/src/api/v1/members/uuid/ban.rs +++ b/src/api/v1/members/uuid/ban.rs @@ -39,8 +39,8 @@ pub async fn post( return Err(Error::Forbidden("Can not ban owner".to_string())); } - let baner = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; - baner + let caller = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; + caller .check_permission(&app_state, Permissions::ManageMember) .await?; From 5fe5186142df61b407815509c21e9fd84babd349 Mon Sep 17 00:00:00 2001 From: BAaboe Date: Wed, 23 Jul 2025 01:31:28 +0200 Subject: [PATCH 133/160] feat: ban permission --- src/api/v1/members/uuid/ban.rs | 2 +- src/objects/role.rs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/api/v1/members/uuid/ban.rs b/src/api/v1/members/uuid/ban.rs index b543975..dd00ecf 100644 --- a/src/api/v1/members/uuid/ban.rs +++ b/src/api/v1/members/uuid/ban.rs @@ -41,7 +41,7 @@ pub async fn post( let caller = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; caller - .check_permission(&app_state, Permissions::ManageMember) + .check_permission(&app_state, Permissions::BanMembers) .await?; member.ban(&mut conn, &payload.reason).await?; diff --git a/src/objects/role.rs b/src/objects/role.rs index ea70686..6e6c88b 100644 --- a/src/objects/role.rs +++ b/src/objects/role.rs @@ -176,6 +176,8 @@ pub enum Permissions { ManageGuild = 32, /// Lets users change member settings (nickname, etc) ManageMember = 64, + /// Lets user ban members + BanMembers = 128, } impl Permissions { From c725d13ca8500abf55ff2d27d89af7e4bb184431 Mon Sep 17 00:00:00 2001 From: BAaboe Date: Wed, 23 Jul 2025 01:35:28 +0200 Subject: [PATCH 134/160] feat: kick permission --- src/api/v1/members/uuid/mod.rs | 2 +- src/objects/role.rs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/api/v1/members/uuid/mod.rs b/src/api/v1/members/uuid/mod.rs index 580c586..0c697c2 100644 --- a/src/api/v1/members/uuid/mod.rs +++ b/src/api/v1/members/uuid/mod.rs @@ -55,7 +55,7 @@ pub async fn delete( let deleter = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; deleter - .check_permission(&app_state, Permissions::ManageMember) + .check_permission(&app_state, Permissions::KickMember) .await?; member.delete(&mut conn).await?; diff --git a/src/objects/role.rs b/src/objects/role.rs index ea70686..5a57e5c 100644 --- a/src/objects/role.rs +++ b/src/objects/role.rs @@ -176,6 +176,8 @@ pub enum Permissions { ManageGuild = 32, /// Lets users change member settings (nickname, etc) ManageMember = 64, + /// Lets users kick members + KickMember = 256, } impl Permissions { @@ -188,6 +190,7 @@ impl Permissions { Self::ManageInvite, Self::ManageGuild, Self::ManageMember, + Self::KickMember, ]; all_perms From ceaa37cbe23564fd56d60773c7a2ea525898d3fc Mon Sep 17 00:00:00 2001 From: BAaboe Date: Wed, 23 Jul 2025 01:37:35 +0200 Subject: [PATCH 135/160] fix: plural fix thing --- src/api/v1/members/uuid/ban.rs | 2 +- src/objects/role.rs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/api/v1/members/uuid/ban.rs b/src/api/v1/members/uuid/ban.rs index dd00ecf..b79da23 100644 --- a/src/api/v1/members/uuid/ban.rs +++ b/src/api/v1/members/uuid/ban.rs @@ -41,7 +41,7 @@ pub async fn post( let caller = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; caller - .check_permission(&app_state, Permissions::BanMembers) + .check_permission(&app_state, Permissions::BanMember) .await?; member.ban(&mut conn, &payload.reason).await?; diff --git a/src/objects/role.rs b/src/objects/role.rs index 6e6c88b..7b078e5 100644 --- a/src/objects/role.rs +++ b/src/objects/role.rs @@ -177,7 +177,7 @@ pub enum Permissions { /// Lets users change member settings (nickname, etc) ManageMember = 64, /// Lets user ban members - BanMembers = 128, + BanMember = 128, } impl Permissions { @@ -190,6 +190,7 @@ impl Permissions { Self::ManageInvite, Self::ManageGuild, Self::ManageMember, + Self::BanMember, ]; all_perms From cbdf6f79e2f55f76fb6174ecd6496d65e58972f9 Mon Sep 17 00:00:00 2001 From: BAaboe Date: Wed, 23 Jul 2025 01:48:31 +0200 Subject: [PATCH 136/160] feat: idiot(goin) proofing --- src/api/v1/members/uuid/ban.rs | 4 ---- src/objects/member.rs | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/api/v1/members/uuid/ban.rs b/src/api/v1/members/uuid/ban.rs index b79da23..5ba6702 100644 --- a/src/api/v1/members/uuid/ban.rs +++ b/src/api/v1/members/uuid/ban.rs @@ -35,10 +35,6 @@ pub async fn post( let member = Member::fetch_one_with_member(&app_state, None, member_uuid).await?; - if member.is_owner { - return Err(Error::Forbidden("Can not ban owner".to_string())); - } - let caller = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; caller .check_permission(&app_state, Permissions::BanMember) diff --git a/src/objects/member.rs b/src/objects/member.rs index d07b701..05ef7bc 100644 --- a/src/objects/member.rs +++ b/src/objects/member.rs @@ -208,6 +208,10 @@ impl Member { } pub async fn delete(self, conn: &mut Conn) -> Result<(), Error> { + if self.is_owner { + return Err(Error::Forbidden("Can not ban owner".to_string())); + } + delete(guild_members::table) .filter(guild_members::uuid.eq(self.uuid)) .execute(conn) From e074ca89f969c0eeead2f4acb9550947a4785d5c Mon Sep 17 00:00:00 2001 From: BAaboe Date: Wed, 23 Jul 2025 01:50:55 +0200 Subject: [PATCH 137/160] feat: idiot proofing --- src/api/v1/members/uuid/mod.rs | 4 ---- src/objects/member.rs | 3 +++ 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/api/v1/members/uuid/mod.rs b/src/api/v1/members/uuid/mod.rs index 0c697c2..734572e 100644 --- a/src/api/v1/members/uuid/mod.rs +++ b/src/api/v1/members/uuid/mod.rs @@ -48,10 +48,6 @@ pub async fn delete( let member = Member::fetch_one_with_member(&app_state, &me, member_uuid).await?; - if member.is_owner { - return Err(Error::Forbidden("Can not kick owner".to_string())); - } - let deleter = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; deleter diff --git a/src/objects/member.rs b/src/objects/member.rs index 8678f4a..dbbeb9d 100644 --- a/src/objects/member.rs +++ b/src/objects/member.rs @@ -188,6 +188,9 @@ impl Member { } pub async fn delete(self, conn: &mut Conn) -> Result<(), Error> { + if member.is_owner { + return Err(Error::Forbidden("Can not kick owner".to_string())) + } delete(guild_members::table) .filter(guild_members::uuid.eq(self.uuid)) .execute(conn) From ac5ca90974ebe9e2c3ebfc5b3c481a7abdde2edc Mon Sep 17 00:00:00 2001 From: BAaboe Date: Wed, 23 Jul 2025 02:03:06 +0200 Subject: [PATCH 138/160] fix: self not member --- src/objects/member.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/objects/member.rs b/src/objects/member.rs index dbbeb9d..ae3b3ef 100644 --- a/src/objects/member.rs +++ b/src/objects/member.rs @@ -188,7 +188,7 @@ impl Member { } pub async fn delete(self, conn: &mut Conn) -> Result<(), Error> { - if member.is_owner { + if self.is_owner { return Err(Error::Forbidden("Can not kick owner".to_string())) } delete(guild_members::table) From 475e0081059bbbad48f87a6ad03f44e33080c4a8 Mon Sep 17 00:00:00 2001 From: Radical Date: Wed, 23 Jul 2025 15:03:56 +0200 Subject: [PATCH 139/160] fix: move owner check to correct function --- src/objects/member.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/objects/member.rs b/src/objects/member.rs index 05ef7bc..50a0a24 100644 --- a/src/objects/member.rs +++ b/src/objects/member.rs @@ -208,10 +208,6 @@ impl Member { } pub async fn delete(self, conn: &mut Conn) -> Result<(), Error> { - if self.is_owner { - return Err(Error::Forbidden("Can not ban owner".to_string())); - } - delete(guild_members::table) .filter(guild_members::uuid.eq(self.uuid)) .execute(conn) @@ -221,6 +217,10 @@ impl Member { } pub async fn ban(self, conn: &mut Conn, reason: &String) -> Result<(), Error> { + if self.is_owner { + return Err(Error::Forbidden("Can not ban owner".to_string())); + } + use guild_bans::dsl; insert_into(guild_bans::table) .values(( From bb8927840de568b76e3d3e3aa6de9c3011967f15 Mon Sep 17 00:00:00 2001 From: Radical Date: Wed, 23 Jul 2025 15:04:02 +0200 Subject: [PATCH 140/160] style: formatting --- src/api/v1/members/uuid/ban.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/api/v1/members/uuid/ban.rs b/src/api/v1/members/uuid/ban.rs index 5ba6702..dfe53f6 100644 --- a/src/api/v1/members/uuid/ban.rs +++ b/src/api/v1/members/uuid/ban.rs @@ -36,6 +36,7 @@ pub async fn post( let member = Member::fetch_one_with_member(&app_state, None, member_uuid).await?; let caller = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; + caller .check_permission(&app_state, Permissions::BanMember) .await?; From 3ad73f28fa95efab19637b9a72f7ed358b612ff5 Mon Sep 17 00:00:00 2001 From: BAaboe Date: Wed, 23 Jul 2025 16:30:38 +0200 Subject: [PATCH 141/160] feat: added ban time --- src/schema.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/schema.rs b/src/schema.rs index 54284d6..e4d6730 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -53,6 +53,7 @@ diesel::table! { user_uuid -> Uuid, #[max_length = 200] reason -> Nullable, + ban_time -> Timestamptz, } } From 7e10086753cad7c635d0a6d4aa4e6333805e6434 Mon Sep 17 00:00:00 2001 From: BAaboe Date: Wed, 23 Jul 2025 19:08:54 +0200 Subject: [PATCH 142/160] feat: retrive all banned users in a guild --- migrations/2025-07-22-195121_add_ban/up.sql | 1 + src/api/v1/guilds/uuid/bans.rs | 24 +++++++++ src/api/v1/guilds/uuid/mod.rs | 3 ++ src/objects/bans.rs | 56 +++++++++++++++++++++ src/objects/member.rs | 14 ++---- src/objects/mod.rs | 2 + 6 files changed, 91 insertions(+), 9 deletions(-) create mode 100644 src/api/v1/guilds/uuid/bans.rs create mode 100644 src/objects/bans.rs diff --git a/migrations/2025-07-22-195121_add_ban/up.sql b/migrations/2025-07-22-195121_add_ban/up.sql index 020a1b0..d8d7fab 100644 --- a/migrations/2025-07-22-195121_add_ban/up.sql +++ b/migrations/2025-07-22-195121_add_ban/up.sql @@ -3,5 +3,6 @@ CREATE TABLE guild_bans ( guild_uuid uuid NOT NULL REFERENCES guilds(uuid) ON DELETE CASCADE, user_uuid uuid NOT NULL REFERENCES users(uuid), reason VARCHAR(200) DEFAULT NULL, + ban_time TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (user_uuid, guild_uuid) ); diff --git a/src/api/v1/guilds/uuid/bans.rs b/src/api/v1/guilds/uuid/bans.rs new file mode 100644 index 0000000..5b29f1d --- /dev/null +++ b/src/api/v1/guilds/uuid/bans.rs @@ -0,0 +1,24 @@ +use std::sync::Arc; + +use axum::{extract::{Path, State}, http::{Extensions, StatusCode}, response::IntoResponse, Extension, Json}; +use uuid::Uuid; + +use crate::{api::v1::auth::CurrentUser, error::Error, objects::{self, GuildBan, Member, Permissions}, utils::global_checks, AppState}; + + +pub async fn get( + State(app_state): State>, + Path(guild_uuid): Path, + Extension(CurrentUser(uuid)): Extension> +) -> Result { + global_checks(&app_state, uuid).await?; + + let mut conn = app_state.pool.get().await?; + + let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; + member.check_permission(&app_state, Permissions::BanMember).await?; + + let all_guild_bans = GuildBan::fetch_all(&mut conn, guild_uuid).await?; + + Ok((StatusCode::OK, Json(all_guild_bans))) +} diff --git a/src/api/v1/guilds/uuid/mod.rs b/src/api/v1/guilds/uuid/mod.rs index 52f0b64..a886e3d 100644 --- a/src/api/v1/guilds/uuid/mod.rs +++ b/src/api/v1/guilds/uuid/mod.rs @@ -16,6 +16,7 @@ mod channels; mod invites; mod members; mod roles; +mod bans; use crate::{ AppState, @@ -42,6 +43,8 @@ pub fn router() -> Router> { .route("/invites", post(invites::create)) // Members .route("/members", get(members::get)) + // Bans + .route("/bans", get(bans::get)) } /// `GET /api/v1/guilds/{uuid}` DESCRIPTION diff --git a/src/objects/bans.rs b/src/objects/bans.rs new file mode 100644 index 0000000..3aaa1ee --- /dev/null +++ b/src/objects/bans.rs @@ -0,0 +1,56 @@ +use diesel::{ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use diesel_async::RunQueryDsl; + +use crate::{ + error::Error, objects::{load_or_empty, Guild}, schema::guild_bans, Conn +}; + + +#[derive(Selectable, Queryable, Serialize, Deserialize)] +#[diesel(table_name = guild_bans)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct GuildBan { + pub guild_uuid: Uuid, + pub user_uuid: Uuid, + pub reason: Option, + pub ban_time: chrono::DateTime, +} + + +impl GuildBan { + pub async fn fetch_one(conn: &mut Conn, guild_uuid: Uuid, user_uuid: Uuid) -> Result { + use guild_bans::dsl; + let guild_ban = dsl::guild_bans + .filter(dsl::guild_uuid.eq(guild_uuid)) + .filter(dsl::user_uuid.eq(user_uuid)) + .select(GuildBan::as_select()) + .get_result(conn) + .await?; + + Ok(guild_ban) + } + + pub async fn fetch_all(conn: &mut Conn, guild_uuid: Uuid) -> Result, Error> { + use guild_bans::dsl; + let all_guild_bans = load_or_empty(dsl::guild_bans + .filter(dsl::guild_uuid.eq(guild_uuid)) + .load(conn) + .await + )?; + + Ok(all_guild_bans) + } + + pub async fn unban(self, conn: &mut Conn) -> Result<(), Error> { + use guild_bans::dsl; + diesel::delete(guild_bans::table) + .filter(dsl::guild_uuid.eq(self.guild_uuid)) + .filter(dsl::user_uuid.eq(self.user_uuid)) + .execute(conn) + .await?; + Ok(()) + } +} diff --git a/src/objects/member.rs b/src/objects/member.rs index 50a0a24..0a216ea 100644 --- a/src/objects/member.rs +++ b/src/objects/member.rs @@ -1,5 +1,5 @@ use diesel::{ - ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, delete, + ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, insert_into, }; use diesel_async::RunQueryDsl; @@ -9,11 +9,12 @@ use uuid::Uuid; use crate::{ AppState, Conn, error::Error, - objects::{Me, Permissions, Role}, + objects::{Me, Permissions, Role, GuildBan}, schema::guild_bans, schema::guild_members, }; + use super::{User, load_or_empty}; #[derive(Serialize, Queryable, Selectable, Insertable)] @@ -74,12 +75,6 @@ pub struct Member { user: User, } -#[derive(Serialize, Deserialize)] -pub struct GuildBan { - pub guild_uuid: Uuid, - pub user_uuid: Uuid, - pub reason: String, -} impl Member { pub async fn count(conn: &mut Conn, guild_uuid: Uuid) -> Result { @@ -208,7 +203,7 @@ impl Member { } pub async fn delete(self, conn: &mut Conn) -> Result<(), Error> { - delete(guild_members::table) + diesel::delete(guild_members::table) .filter(guild_members::uuid.eq(self.uuid)) .execute(conn) .await?; @@ -235,4 +230,5 @@ impl Member { Ok(()) } + } diff --git a/src/objects/mod.rs b/src/objects/mod.rs index 4af16d8..e53583e 100644 --- a/src/objects/mod.rs +++ b/src/objects/mod.rs @@ -18,6 +18,7 @@ mod message; mod password_reset_token; mod role; mod user; +mod bans; pub use channel::Channel; pub use email_token::EmailToken; @@ -32,6 +33,7 @@ pub use password_reset_token::PasswordResetToken; pub use role::Permissions; pub use role::Role; pub use user::User; +pub use bans::GuildBan; use crate::error::Error; From 26f528819e87afade0021748caa7d1a19d364072 Mon Sep 17 00:00:00 2001 From: BAaboe Date: Wed, 23 Jul 2025 19:10:17 +0200 Subject: [PATCH 143/160] style: cargo clippy --fix && cargo fmt --- src/api/v1/guilds/uuid/bans.rs | 24 ++++++++++++++++++------ src/api/v1/guilds/uuid/mod.rs | 2 +- src/objects/bans.rs | 25 +++++++++++++------------ src/objects/member.rs | 8 ++------ src/objects/mod.rs | 4 ++-- 5 files changed, 36 insertions(+), 27 deletions(-) diff --git a/src/api/v1/guilds/uuid/bans.rs b/src/api/v1/guilds/uuid/bans.rs index 5b29f1d..44ed48d 100644 --- a/src/api/v1/guilds/uuid/bans.rs +++ b/src/api/v1/guilds/uuid/bans.rs @@ -1,24 +1,36 @@ use std::sync::Arc; -use axum::{extract::{Path, State}, http::{Extensions, StatusCode}, response::IntoResponse, Extension, Json}; +use axum::{ + Extension, Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; use uuid::Uuid; -use crate::{api::v1::auth::CurrentUser, error::Error, objects::{self, GuildBan, Member, Permissions}, utils::global_checks, AppState}; - +use crate::{ + AppState, + api::v1::auth::CurrentUser, + error::Error, + objects::{GuildBan, Member, Permissions}, + utils::global_checks, +}; pub async fn get( State(app_state): State>, Path(guild_uuid): Path, - Extension(CurrentUser(uuid)): Extension> + Extension(CurrentUser(uuid)): Extension>, ) -> Result { global_checks(&app_state, uuid).await?; let mut conn = app_state.pool.get().await?; let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; - member.check_permission(&app_state, Permissions::BanMember).await?; + member + .check_permission(&app_state, Permissions::BanMember) + .await?; let all_guild_bans = GuildBan::fetch_all(&mut conn, guild_uuid).await?; - + Ok((StatusCode::OK, Json(all_guild_bans))) } diff --git a/src/api/v1/guilds/uuid/mod.rs b/src/api/v1/guilds/uuid/mod.rs index a886e3d..2174932 100644 --- a/src/api/v1/guilds/uuid/mod.rs +++ b/src/api/v1/guilds/uuid/mod.rs @@ -12,11 +12,11 @@ use axum::{ use bytes::Bytes; use uuid::Uuid; +mod bans; mod channels; mod invites; mod members; mod roles; -mod bans; use crate::{ AppState, diff --git a/src/objects/bans.rs b/src/objects/bans.rs index 3aaa1ee..9eb58f5 100644 --- a/src/objects/bans.rs +++ b/src/objects/bans.rs @@ -4,10 +4,7 @@ use uuid::Uuid; use diesel_async::RunQueryDsl; -use crate::{ - error::Error, objects::{load_or_empty, Guild}, schema::guild_bans, Conn -}; - +use crate::{Conn, error::Error, objects::load_or_empty, schema::guild_bans}; #[derive(Selectable, Queryable, Serialize, Deserialize)] #[diesel(table_name = guild_bans)] @@ -19,9 +16,12 @@ pub struct GuildBan { pub ban_time: chrono::DateTime, } - impl GuildBan { - pub async fn fetch_one(conn: &mut Conn, guild_uuid: Uuid, user_uuid: Uuid) -> Result { + pub async fn fetch_one( + conn: &mut Conn, + guild_uuid: Uuid, + user_uuid: Uuid, + ) -> Result { use guild_bans::dsl; let guild_ban = dsl::guild_bans .filter(dsl::guild_uuid.eq(guild_uuid)) @@ -35,12 +35,13 @@ impl GuildBan { pub async fn fetch_all(conn: &mut Conn, guild_uuid: Uuid) -> Result, Error> { use guild_bans::dsl; - let all_guild_bans = load_or_empty(dsl::guild_bans - .filter(dsl::guild_uuid.eq(guild_uuid)) - .load(conn) - .await - )?; - + let all_guild_bans = load_or_empty( + dsl::guild_bans + .filter(dsl::guild_uuid.eq(guild_uuid)) + .load(conn) + .await, + )?; + Ok(all_guild_bans) } diff --git a/src/objects/member.rs b/src/objects/member.rs index 0a216ea..fcd0b6b 100644 --- a/src/objects/member.rs +++ b/src/objects/member.rs @@ -1,6 +1,5 @@ use diesel::{ - ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, - insert_into, + ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, insert_into, }; use diesel_async::RunQueryDsl; use serde::{Deserialize, Serialize}; @@ -9,12 +8,11 @@ use uuid::Uuid; use crate::{ AppState, Conn, error::Error, - objects::{Me, Permissions, Role, GuildBan}, + objects::{Me, Permissions, Role}, schema::guild_bans, schema::guild_members, }; - use super::{User, load_or_empty}; #[derive(Serialize, Queryable, Selectable, Insertable)] @@ -75,7 +73,6 @@ pub struct Member { user: User, } - impl Member { pub async fn count(conn: &mut Conn, guild_uuid: Uuid) -> Result { use guild_members::dsl; @@ -230,5 +227,4 @@ impl Member { Ok(()) } - } diff --git a/src/objects/mod.rs b/src/objects/mod.rs index e53583e..3bcce9c 100644 --- a/src/objects/mod.rs +++ b/src/objects/mod.rs @@ -7,6 +7,7 @@ use log::debug; use serde::Deserialize; use uuid::Uuid; +mod bans; mod channel; mod email_token; mod friends; @@ -18,8 +19,8 @@ mod message; mod password_reset_token; mod role; mod user; -mod bans; +pub use bans::GuildBan; pub use channel::Channel; pub use email_token::EmailToken; pub use friends::Friend; @@ -33,7 +34,6 @@ pub use password_reset_token::PasswordResetToken; pub use role::Permissions; pub use role::Role; pub use user::User; -pub use bans::GuildBan; use crate::error::Error; From c2b5f6568f08d62cdcb55763609787d637394cbd Mon Sep 17 00:00:00 2001 From: BAaboe Date: Thu, 24 Jul 2025 01:58:10 +0200 Subject: [PATCH 144/160] style: renaming parameter, ban_time -> banned_since --- migrations/2025-07-22-195121_add_ban/up.sql | 2 +- src/objects/bans.rs | 2 +- src/schema.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/migrations/2025-07-22-195121_add_ban/up.sql b/migrations/2025-07-22-195121_add_ban/up.sql index d8d7fab..a590142 100644 --- a/migrations/2025-07-22-195121_add_ban/up.sql +++ b/migrations/2025-07-22-195121_add_ban/up.sql @@ -3,6 +3,6 @@ CREATE TABLE guild_bans ( guild_uuid uuid NOT NULL REFERENCES guilds(uuid) ON DELETE CASCADE, user_uuid uuid NOT NULL REFERENCES users(uuid), reason VARCHAR(200) DEFAULT NULL, - ban_time TIMESTAMPTZ NOT NULL DEFAULT NOW(), + banned_since TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (user_uuid, guild_uuid) ); diff --git a/src/objects/bans.rs b/src/objects/bans.rs index 9eb58f5..602afa6 100644 --- a/src/objects/bans.rs +++ b/src/objects/bans.rs @@ -13,7 +13,7 @@ pub struct GuildBan { pub guild_uuid: Uuid, pub user_uuid: Uuid, pub reason: Option, - pub ban_time: chrono::DateTime, + pub banned_since: chrono::DateTime, } impl GuildBan { diff --git a/src/schema.rs b/src/schema.rs index e4d6730..422c3a3 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -53,7 +53,7 @@ diesel::table! { user_uuid -> Uuid, #[max_length = 200] reason -> Nullable, - ban_time -> Timestamptz, + banned_since -> Timestamptz, } } From ba2442e7860bbd4993dbcb3069505b99f1410c60 Mon Sep 17 00:00:00 2001 From: BAaboe Date: Thu, 24 Jul 2025 02:13:53 +0200 Subject: [PATCH 145/160] style: updated to use the new ban object --- src/objects/member.rs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/objects/member.rs b/src/objects/member.rs index fcd0b6b..ed6a77c 100644 --- a/src/objects/member.rs +++ b/src/objects/member.rs @@ -6,11 +6,7 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ - AppState, Conn, - error::Error, - objects::{Me, Permissions, Role}, - schema::guild_bans, - schema::guild_members, + error::Error, objects::{GuildBan, Me, Permissions, Role}, schema::{guild_bans, guild_members}, AppState, Conn }; use super::{User, load_or_empty}; @@ -169,15 +165,10 @@ impl Member { ) -> Result { let mut conn = app_state.pool.get().await?; - use guild_bans::dsl; - let banned = dsl::guild_bans - .filter(guild_bans::guild_uuid.eq(guild_uuid)) - .filter(guild_bans::user_uuid.eq(user_uuid)) - .execute(&mut conn) - .await; + let banned = GuildBan::fetch_one(&mut conn, guild_uuid, user_uuid).await; match banned { Ok(_) => Err(Error::Forbidden("User banned".to_string())), - Err(diesel::result::Error::NotFound) => Ok(()), + Err(Error::SqlError(diesel::result::Error::NotFound)) => Ok(()), Err(e) => Err(e.into()), }?; From 0e0c590e4dd0260382a6be381974e5b9244c668b Mon Sep 17 00:00:00 2001 From: BAaboe Date: Thu, 24 Jul 2025 02:14:29 +0200 Subject: [PATCH 146/160] feat: added unband endpoint --- src/api/v1/guilds/uuid/bans.rs | 23 +++++++++++++++++++++-- src/api/v1/guilds/uuid/mod.rs | 3 ++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/api/v1/guilds/uuid/bans.rs b/src/api/v1/guilds/uuid/bans.rs index 44ed48d..52e0949 100644 --- a/src/api/v1/guilds/uuid/bans.rs +++ b/src/api/v1/guilds/uuid/bans.rs @@ -25,8 +25,8 @@ pub async fn get( let mut conn = app_state.pool.get().await?; - let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; - member + let caller = Member::check_membership(&mut conn, uuid, guild_uuid).await?; + caller .check_permission(&app_state, Permissions::BanMember) .await?; @@ -34,3 +34,22 @@ pub async fn get( Ok((StatusCode::OK, Json(all_guild_bans))) } + +pub async fn unban( + State(app_state): State>, + Path((guild_uuid, user_uuid)): Path<(Uuid, Uuid)>, + Extension(CurrentUser(uuid)): Extension>, +) -> Result { + global_checks(&app_state, uuid).await?; + + let mut conn = app_state.pool.get().await?; + + let caller = Member::check_membership(&mut conn, uuid, guild_uuid).await?; + caller.check_permission(&app_state, Permissions::BanMember).await?; + + let ban = GuildBan::fetch_one(&mut conn, guild_uuid, user_uuid).await?; + + ban.unban(&mut conn).await?; + + Ok(StatusCode::OK) +} diff --git a/src/api/v1/guilds/uuid/mod.rs b/src/api/v1/guilds/uuid/mod.rs index 2174932..b9d8abf 100644 --- a/src/api/v1/guilds/uuid/mod.rs +++ b/src/api/v1/guilds/uuid/mod.rs @@ -7,7 +7,7 @@ use axum::{ extract::{Multipart, Path, State}, http::StatusCode, response::IntoResponse, - routing::{get, patch, post}, + routing::{get, patch, post, delete}, }; use bytes::Bytes; use uuid::Uuid; @@ -45,6 +45,7 @@ pub fn router() -> Router> { .route("/members", get(members::get)) // Bans .route("/bans", get(bans::get)) + .route("/bans/{uuid}", delete(bans::unban)) } /// `GET /api/v1/guilds/{uuid}` DESCRIPTION From b28d5b840ff593eb1b59fdf9c0e7644577d3c002 Mon Sep 17 00:00:00 2001 From: BAaboe Date: Thu, 24 Jul 2025 02:30:52 +0200 Subject: [PATCH 147/160] style: cargo clippy --fix && cargo fmt --- src/api/v1/guilds/uuid/bans.rs | 4 +++- src/api/v1/guilds/uuid/mod.rs | 2 +- src/objects/member.rs | 7 +++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/api/v1/guilds/uuid/bans.rs b/src/api/v1/guilds/uuid/bans.rs index 52e0949..29d5a05 100644 --- a/src/api/v1/guilds/uuid/bans.rs +++ b/src/api/v1/guilds/uuid/bans.rs @@ -45,7 +45,9 @@ pub async fn unban( let mut conn = app_state.pool.get().await?; let caller = Member::check_membership(&mut conn, uuid, guild_uuid).await?; - caller.check_permission(&app_state, Permissions::BanMember).await?; + caller + .check_permission(&app_state, Permissions::BanMember) + .await?; let ban = GuildBan::fetch_one(&mut conn, guild_uuid, user_uuid).await?; diff --git a/src/api/v1/guilds/uuid/mod.rs b/src/api/v1/guilds/uuid/mod.rs index b9d8abf..65a7c76 100644 --- a/src/api/v1/guilds/uuid/mod.rs +++ b/src/api/v1/guilds/uuid/mod.rs @@ -7,7 +7,7 @@ use axum::{ extract::{Multipart, Path, State}, http::StatusCode, response::IntoResponse, - routing::{get, patch, post, delete}, + routing::{delete, get, patch, post}, }; use bytes::Bytes; use uuid::Uuid; diff --git a/src/objects/member.rs b/src/objects/member.rs index ed6a77c..6310de3 100644 --- a/src/objects/member.rs +++ b/src/objects/member.rs @@ -6,7 +6,10 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ - error::Error, objects::{GuildBan, Me, Permissions, Role}, schema::{guild_bans, guild_members}, AppState, Conn + AppState, Conn, + error::Error, + objects::{GuildBan, Me, Permissions, Role}, + schema::{guild_bans, guild_members}, }; use super::{User, load_or_empty}; @@ -169,7 +172,7 @@ impl Member { match banned { Ok(_) => Err(Error::Forbidden("User banned".to_string())), Err(Error::SqlError(diesel::result::Error::NotFound)) => Ok(()), - Err(e) => Err(e.into()), + Err(e) => Err(e), }?; let member_uuid = Uuid::now_v7(); From b38b5360f670d3f5c67b047cb9761171635847f4 Mon Sep 17 00:00:00 2001 From: BAaboe Date: Fri, 25 Jul 2025 00:21:20 +0200 Subject: [PATCH 148/160] style: style --- src/objects/member.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/objects/member.rs b/src/objects/member.rs index 6310de3..aaa8347 100644 --- a/src/objects/member.rs +++ b/src/objects/member.rs @@ -1,5 +1,5 @@ use diesel::{ - ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, insert_into, + ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, insert_into, delete, }; use diesel_async::RunQueryDsl; use serde::{Deserialize, Serialize}; @@ -194,7 +194,7 @@ impl Member { } pub async fn delete(self, conn: &mut Conn) -> Result<(), Error> { - diesel::delete(guild_members::table) + delete(guild_members::table) .filter(guild_members::uuid.eq(self.uuid)) .execute(conn) .await?; From 4a2f98a1805f9adac3969744d4e55368fed81b69 Mon Sep 17 00:00:00 2001 From: BAaboe Date: Fri, 25 Jul 2025 00:21:51 +0200 Subject: [PATCH 149/160] style: cargo clippy --fix && cargo fmt --- src/objects/member.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/objects/member.rs b/src/objects/member.rs index aaa8347..621097d 100644 --- a/src/objects/member.rs +++ b/src/objects/member.rs @@ -1,5 +1,6 @@ use diesel::{ - ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, insert_into, delete, + ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, delete, + insert_into, }; use diesel_async::RunQueryDsl; use serde::{Deserialize, Serialize}; From 3816af56e34484d75b7bd5b3e8dc6354ec643355 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 31 Jul 2025 16:01:10 +0200 Subject: [PATCH 150/160] feat: return role with member --- .../down.sql | 5 ++ .../2025-07-31-133510_roles_uuid_index/up.sql | 5 ++ src/api/v1/members/uuid/ban.rs | 3 +- src/api/v1/members/uuid/mod.rs | 8 ++- src/objects/member.rs | 26 ++++++++-- src/objects/role.rs | 49 +++++++------------ src/schema.rs | 4 +- 7 files changed, 59 insertions(+), 41 deletions(-) create mode 100644 migrations/2025-07-31-133510_roles_uuid_index/down.sql create mode 100644 migrations/2025-07-31-133510_roles_uuid_index/up.sql diff --git a/migrations/2025-07-31-133510_roles_uuid_index/down.sql b/migrations/2025-07-31-133510_roles_uuid_index/down.sql new file mode 100644 index 0000000..efe3f3f --- /dev/null +++ b/migrations/2025-07-31-133510_roles_uuid_index/down.sql @@ -0,0 +1,5 @@ +-- This file should undo anything in `up.sql` +DROP INDEX roles_guuid_uuid; +ALTER TABLE roles DROP CONSTRAINT roles_pkey; +CREATE UNIQUE INDEX roles_pkey ON roles (uuid, guild_uuid); +ALTER TABLE roles ADD PRIMARY KEY USING INDEX roles_pkey; diff --git a/migrations/2025-07-31-133510_roles_uuid_index/up.sql b/migrations/2025-07-31-133510_roles_uuid_index/up.sql new file mode 100644 index 0000000..792e7fd --- /dev/null +++ b/migrations/2025-07-31-133510_roles_uuid_index/up.sql @@ -0,0 +1,5 @@ +-- Your SQL goes here +ALTER TABLE roles DROP CONSTRAINT roles_pkey; +CREATE UNIQUE INDEX roles_pkey ON roles (uuid); +ALTER TABLE roles ADD PRIMARY KEY USING INDEX roles_pkey; +CREATE UNIQUE INDEX roles_guuid_uuid ON roles (uuid, guild_uuid); \ No newline at end of file diff --git a/src/api/v1/members/uuid/ban.rs b/src/api/v1/members/uuid/ban.rs index b959efa..888dca6 100644 --- a/src/api/v1/members/uuid/ban.rs +++ b/src/api/v1/members/uuid/ban.rs @@ -33,7 +33,8 @@ pub async fn post( global_checks(&mut conn, &app_state.config, uuid).await?; - let member = Member::fetch_one_with_member(&mut conn, &app_state.cache_pool, None, member_uuid).await?; + let member = + Member::fetch_one_with_member(&mut conn, &app_state.cache_pool, None, member_uuid).await?; let caller = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; diff --git a/src/api/v1/members/uuid/mod.rs b/src/api/v1/members/uuid/mod.rs index 2bdd1ba..0832192 100644 --- a/src/api/v1/members/uuid/mod.rs +++ b/src/api/v1/members/uuid/mod.rs @@ -31,7 +31,9 @@ pub async fn get( let me = Me::get(&mut conn, uuid).await?; - let member = Member::fetch_one_with_member(&mut conn, &app_state.cache_pool, Some(&me), member_uuid).await?; + let member = + Member::fetch_one_with_member(&mut conn, &app_state.cache_pool, Some(&me), member_uuid) + .await?; Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; Ok((StatusCode::OK, Json(member))) @@ -48,7 +50,9 @@ pub async fn delete( let me = Me::get(&mut conn, uuid).await?; - let member = Member::fetch_one_with_member(&mut conn, &app_state.cache_pool, Some(&me), member_uuid).await?; + let member = + Member::fetch_one_with_member(&mut conn, &app_state.cache_pool, Some(&me), member_uuid) + .await?; let deleter = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; diff --git a/src/objects/member.rs b/src/objects/member.rs index 3eb8c4d..a25bf66 100644 --- a/src/objects/member.rs +++ b/src/objects/member.rs @@ -1,6 +1,6 @@ use diesel::{ - ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, delete, - insert_into, + ExpressionMethods, Identifiable, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, + delete, insert_into, }; use diesel_async::RunQueryDsl; use serde::{Deserialize, Serialize}; @@ -15,8 +15,9 @@ use crate::{ use super::{User, load_or_empty}; -#[derive(Serialize, Queryable, Selectable, Insertable)] +#[derive(Serialize, Queryable, Identifiable, Selectable, Insertable)] #[diesel(table_name = guild_members)] +#[diesel(primary_key(uuid))] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct MemberBuilder { pub uuid: Uuid, @@ -41,6 +42,8 @@ impl MemberBuilder { user = User::fetch_one(conn, cache_pool, self.user_uuid).await?; } + let roles = Role::fetch_from_member(conn, cache_pool, self).await?; + Ok(Member { uuid: self.uuid, nickname: self.nickname.clone(), @@ -48,6 +51,7 @@ impl MemberBuilder { guild_uuid: self.guild_uuid, is_owner: self.is_owner, user, + roles, }) } @@ -58,7 +62,7 @@ impl MemberBuilder { permission: Permissions, ) -> Result<(), Error> { if !self.is_owner { - let roles = Role::fetch_from_member(conn, cache_pool, self.uuid).await?; + let roles = Role::fetch_from_member(conn, cache_pool, self).await?; let allowed = roles.iter().any(|r| r.permissions & permission as i64 != 0); if !allowed { return Err(Error::Forbidden("Not allowed".to_string())); @@ -73,10 +77,12 @@ impl MemberBuilder { pub struct Member { pub uuid: Uuid, pub nickname: Option, + #[serde(skip)] pub user_uuid: Uuid, pub guild_uuid: Uuid, pub is_owner: bool, user: User, + roles: Vec, } impl Member { @@ -199,7 +205,7 @@ impl Member { pub async fn delete(self, conn: &mut Conn) -> Result<(), Error> { if self.is_owner { - return Err(Error::Forbidden("Can not kick owner".to_string())) + return Err(Error::Forbidden("Can not kick owner".to_string())); } delete(guild_members::table) .filter(guild_members::uuid.eq(self.uuid)) @@ -228,4 +234,14 @@ impl Member { Ok(()) } + + pub fn to_builder(&self) -> MemberBuilder { + MemberBuilder { + uuid: self.uuid, + nickname: self.nickname.clone(), + user_uuid: self.user_uuid, + guild_uuid: self.guild_uuid, + is_owner: self.is_owner, + } + } } diff --git a/src/objects/role.rs b/src/objects/role.rs index 46f54f6..cc71fb2 100644 --- a/src/objects/role.rs +++ b/src/objects/role.rs @@ -1,6 +1,7 @@ +use diesel::query_dsl::BelongingToDsl; use diesel::{ - ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, insert_into, - update, + Associations, ExpressionMethods, Identifiable, Insertable, QueryDsl, Queryable, Selectable, + SelectableHelper, insert_into, update, }; use diesel_async::RunQueryDsl; use serde::{Deserialize, Serialize}; @@ -13,10 +14,11 @@ use crate::{ utils::{CacheFns, order_by_is_above}, }; -use super::{HasIsAbove, HasUuid, load_or_empty}; +use super::{HasIsAbove, HasUuid, load_or_empty, member::MemberBuilder}; -#[derive(Deserialize, Serialize, Clone, Queryable, Selectable, Insertable)] +#[derive(Deserialize, Serialize, Clone, Identifiable, Queryable, Selectable, Insertable)] #[diesel(table_name = roles)] +#[diesel(primary_key(uuid))] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct Role { uuid: Uuid, @@ -27,27 +29,17 @@ pub struct Role { pub permissions: i64, } -#[derive(Serialize, Clone, Queryable, Selectable, Insertable)] +#[derive(Serialize, Clone, Identifiable, Queryable, Selectable, Insertable, Associations)] #[diesel(table_name = role_members)] +#[diesel(belongs_to(MemberBuilder, foreign_key = member_uuid))] +#[diesel(belongs_to(Role, foreign_key = role_uuid))] +#[diesel(primary_key(role_uuid, member_uuid))] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct RoleMember { role_uuid: Uuid, member_uuid: Uuid, } -impl RoleMember { - async fn fetch_role(&self, conn: &mut Conn) -> Result { - use roles::dsl; - let role: Role = dsl::roles - .filter(dsl::uuid.eq(self.role_uuid)) - .select(Role::as_select()) - .get_result(conn) - .await?; - - Ok(role) - } -} - impl HasUuid for Role { fn uuid(&self) -> &Uuid { self.uuid.as_ref() @@ -77,32 +69,25 @@ impl Role { pub async fn fetch_from_member( conn: &mut Conn, cache_pool: &redis::Client, - member_uuid: Uuid, + member: &MemberBuilder, ) -> Result, Error> { if let Ok(roles) = cache_pool - .get_cache_key(format!("{member_uuid}_roles")) + .get_cache_key(format!("{}_roles", member.uuid)) .await { return Ok(roles); } - use role_members::dsl; - let role_memberships: Vec = load_or_empty( - dsl::role_members - .filter(dsl::member_uuid.eq(member_uuid)) - .select(RoleMember::as_select()) + let roles: Vec = load_or_empty( + RoleMember::belonging_to(member) + .inner_join(roles::table) + .select(Role::as_select()) .load(conn) .await, )?; - let mut roles = vec![]; - - for membership in role_memberships { - roles.push(membership.fetch_role(conn).await?); - } - cache_pool - .set_cache_key(format!("{member_uuid}_roles"), roles.clone(), 300) + .set_cache_key(format!("{}_roles", member.uuid), roles.clone(), 300) .await?; Ok(roles) diff --git a/src/schema.rs b/src/schema.rs index 422c3a3..413f3f1 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -126,7 +126,7 @@ diesel::table! { } diesel::table! { - roles (uuid, guild_uuid) { + roles (uuid) { uuid -> Uuid, guild_uuid -> Uuid, #[max_length = 50] @@ -163,6 +163,7 @@ diesel::table! { diesel::joinable!(access_tokens -> refresh_tokens (refresh_token)); diesel::joinable!(access_tokens -> users (uuid)); diesel::joinable!(channel_permissions -> channels (channel_uuid)); +diesel::joinable!(channel_permissions -> roles (role_uuid)); diesel::joinable!(channels -> guilds (guild_uuid)); diesel::joinable!(guild_bans -> guilds (guild_uuid)); diesel::joinable!(guild_bans -> users (user_uuid)); @@ -175,6 +176,7 @@ diesel::joinable!(messages -> channels (channel_uuid)); diesel::joinable!(messages -> users (user_uuid)); diesel::joinable!(refresh_tokens -> users (uuid)); diesel::joinable!(role_members -> guild_members (member_uuid)); +diesel::joinable!(role_members -> roles (role_uuid)); diesel::joinable!(roles -> guilds (guild_uuid)); diesel::allow_tables_to_appear_in_same_query!( From 314b9ee011f5f6bbc5f715664fc16d2fa12a98a0 Mon Sep 17 00:00:00 2001 From: BAaboe Date: Mon, 4 Aug 2025 20:07:46 +0200 Subject: [PATCH 151/160] feat: added online_status column to users table --- migrations/2025-08-04-180235_add_status_to_user/down.sql | 2 ++ migrations/2025-08-04-180235_add_status_to_user/up.sql | 2 ++ src/schema.rs | 1 + 3 files changed, 5 insertions(+) create mode 100644 migrations/2025-08-04-180235_add_status_to_user/down.sql create mode 100644 migrations/2025-08-04-180235_add_status_to_user/up.sql diff --git a/migrations/2025-08-04-180235_add_status_to_user/down.sql b/migrations/2025-08-04-180235_add_status_to_user/down.sql new file mode 100644 index 0000000..163f7f1 --- /dev/null +++ b/migrations/2025-08-04-180235_add_status_to_user/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE users DROP COLUMN online_status; diff --git a/migrations/2025-08-04-180235_add_status_to_user/up.sql b/migrations/2025-08-04-180235_add_status_to_user/up.sql new file mode 100644 index 0000000..ac16d77 --- /dev/null +++ b/migrations/2025-08-04-180235_add_status_to_user/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE users ADD COLUMN online_status INT2 NOT NULL DEFAULT 0; diff --git a/src/schema.rs b/src/schema.rs index 413f3f1..88f6155 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -157,6 +157,7 @@ diesel::table! { pronouns -> Nullable, #[max_length = 200] about -> Nullable, + online_status -> Int2, } } From 027649a0608fce91f9f995c1fd9398b93897fd1f Mon Sep 17 00:00:00 2001 From: BAaboe Date: Mon, 4 Aug 2025 20:46:49 +0200 Subject: [PATCH 152/160] feat: added online status --- src/api/v1/me/mod.rs | 6 ++++++ src/objects/me.rs | 32 +++++++++++++++++++++++++++++++- src/objects/user.rs | 3 +++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/api/v1/me/mod.rs b/src/api/v1/me/mod.rs index 86d3d9e..9d75d26 100644 --- a/src/api/v1/me/mod.rs +++ b/src/api/v1/me/mod.rs @@ -49,6 +49,7 @@ struct NewInfo { email: Option, pronouns: Option, about: Option, + online_status: Option, } pub async fn update( @@ -110,5 +111,10 @@ pub async fn update( .await?; } + if let Some(online_status) = &json.online_status { + me.set_online_status(&mut conn, &app_state.cache_pool, *online_status) + .await?; + } + Ok(StatusCode::OK) } diff --git a/src/objects/me.rs b/src/objects/me.rs index d03e08b..e76802c 100644 --- a/src/objects/me.rs +++ b/src/objects/me.rs @@ -1,4 +1,4 @@ -use axum::body::Bytes; +use axum::{body::Bytes, http::StatusCode}; use diesel::{ ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper, delete, insert_into, update, @@ -29,6 +29,7 @@ pub struct Me { avatar: Option, pronouns: Option, about: Option, + online_status: i16, pub email: String, pub email_verified: bool, } @@ -277,6 +278,35 @@ impl Me { Ok(()) } + pub async fn set_online_status( + &mut self, + conn: &mut Conn, + cache_pool: &redis::Client, + new_status: i16, + ) -> Result<(), Error> { + if new_status > 4 && new_status < 0 { + return Err(Error::BadRequest("Invalid status code".to_string())); + } + self.online_status = new_status; + + use users::dsl; + update(users::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(dsl::online_status.eq(new_status)) + .execute(conn) + .await?; + + if cache_pool + .get_cache_key::(self.uuid.to_string()) + .await + .is_ok() + { + cache_pool.del_cache_key(self.uuid.to_string()).await? + } + + Ok(()) + } + pub async fn friends_with( &self, conn: &mut Conn, diff --git a/src/objects/user.rs b/src/objects/user.rs index a686c39..e9f638a 100644 --- a/src/objects/user.rs +++ b/src/objects/user.rs @@ -18,6 +18,7 @@ pub struct UserBuilder { avatar: Option, pronouns: Option, about: Option, + online_status: i16, } impl UserBuilder { @@ -29,6 +30,7 @@ impl UserBuilder { avatar: self.avatar, pronouns: self.pronouns, about: self.about, + online_status: self.online_status, friends_since: None, } } @@ -42,6 +44,7 @@ pub struct User { avatar: Option, pronouns: Option, about: Option, + online_status: i16, friends_since: Option>, } From 53451e67c7fe83648cac8170be2f4aafe6306809 Mon Sep 17 00:00:00 2001 From: BAaboe Date: Mon, 4 Aug 2025 20:52:19 +0200 Subject: [PATCH 153/160] fix: 4 is always bigger than 0 --- src/objects/me.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/objects/me.rs b/src/objects/me.rs index e76802c..0c54570 100644 --- a/src/objects/me.rs +++ b/src/objects/me.rs @@ -1,4 +1,4 @@ -use axum::{body::Bytes, http::StatusCode}; +use axum::body::Bytes; use diesel::{ ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper, delete, insert_into, update, @@ -284,7 +284,7 @@ impl Me { cache_pool: &redis::Client, new_status: i16, ) -> Result<(), Error> { - if new_status > 4 && new_status < 0 { + if new_status > 4 || new_status < 0 { return Err(Error::BadRequest("Invalid status code".to_string())); } self.online_status = new_status; From e9cc2a3f0e06e973ff7163b93e286538daea7116 Mon Sep 17 00:00:00 2001 From: Radical Date: Mon, 4 Aug 2025 22:55:22 +0200 Subject: [PATCH 154/160] feat: faster member fetching and pagination --- src/api/v1/guilds/uuid/members.rs | 14 ++- src/objects/member.rs | 190 ++++++++++++++++++++++++++---- src/objects/mod.rs | 16 ++- src/objects/user.rs | 6 +- 4 files changed, 193 insertions(+), 33 deletions(-) diff --git a/src/api/v1/guilds/uuid/members.rs b/src/api/v1/guilds/uuid/members.rs index 56710af..0e1d2bc 100644 --- a/src/api/v1/guilds/uuid/members.rs +++ b/src/api/v1/guilds/uuid/members.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use ::uuid::Uuid; use axum::{ Extension, Json, - extract::{Path, State}, + extract::{Path, Query, State}, http::StatusCode, response::IntoResponse, }; @@ -12,13 +12,14 @@ use crate::{ AppState, api::v1::auth::CurrentUser, error::Error, - objects::{Me, Member}, + objects::{Me, Member, PaginationRequest}, utils::global_checks, }; pub async fn get( State(app_state): State>, Path(guild_uuid): Path, + Query(pagination): Query, Extension(CurrentUser(uuid)): Extension>, ) -> Result { let mut conn = app_state.pool.get().await?; @@ -29,7 +30,14 @@ pub async fn get( let me = Me::get(&mut conn, uuid).await?; - let members = Member::fetch_all(&mut conn, &app_state.cache_pool, &me, guild_uuid).await?; + let members = Member::fetch_page( + &mut conn, + &app_state.cache_pool, + &me, + guild_uuid, + pagination, + ) + .await?; Ok((StatusCode::OK, Json(members))) } diff --git a/src/objects/member.rs b/src/objects/member.rs index a25bf66..59929cd 100644 --- a/src/objects/member.rs +++ b/src/objects/member.rs @@ -1,6 +1,7 @@ use diesel::{ - ExpressionMethods, Identifiable, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, - delete, insert_into, + Associations, BoolExpressionMethods, ExpressionMethods, Identifiable, Insertable, JoinOnDsl, + QueryDsl, Queryable, Selectable, SelectableHelper, define_sql_function, delete, insert_into, + sql_types::{Nullable, VarChar}, }; use diesel_async::RunQueryDsl; use serde::{Deserialize, Serialize}; @@ -9,14 +10,21 @@ use uuid::Uuid; use crate::{ Conn, error::Error, - objects::{GuildBan, Me, Permissions, Role}, - schema::{guild_bans, guild_members}, + objects::PaginationRequest, + schema::{friends, guild_bans, guild_members, users}, }; -use super::{User, load_or_empty}; +use super::{ + Friend, Guild, GuildBan, Me, Pagination, Permissions, Role, User, load_or_empty, + user::UserBuilder, +}; -#[derive(Serialize, Queryable, Identifiable, Selectable, Insertable)] +define_sql_function! { fn coalesce(x: Nullable, y: Nullable, z: VarChar) -> Text; } + +#[derive(Serialize, Queryable, Identifiable, Selectable, Insertable, Associations)] #[diesel(table_name = guild_members)] +#[diesel(belongs_to(UserBuilder, foreign_key = user_uuid))] +#[diesel(belongs_to(Guild, foreign_key = guild_uuid))] #[diesel(primary_key(uuid))] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct MemberBuilder { @@ -55,6 +63,32 @@ impl MemberBuilder { }) } + async fn build_with_parts( + &self, + conn: &mut Conn, + cache_pool: &redis::Client, + user_builder: UserBuilder, + friend: Option, + ) -> Result { + let mut user = user_builder.build(); + + if let Some(friend) = friend { + user.friends_since = Some(friend.accepted_at); + } + + let roles = Role::fetch_from_member(conn, cache_pool, self).await?; + + Ok(Member { + uuid: self.uuid, + nickname: self.nickname.clone(), + user_uuid: self.user_uuid, + guild_uuid: self.guild_uuid, + is_owner: self.is_owner, + user, + roles, + }) + } + pub async fn check_permission( &self, conn: &mut Conn, @@ -120,15 +154,35 @@ impl Member { user_uuid: Uuid, guild_uuid: Uuid, ) -> Result { + use friends::dsl as fdsl; use guild_members::dsl; - let member: MemberBuilder = dsl::guild_members - .filter(dsl::user_uuid.eq(user_uuid)) - .filter(dsl::guild_uuid.eq(guild_uuid)) - .select(MemberBuilder::as_select()) - .get_result(conn) - .await?; + let (member, user, friend): (MemberBuilder, UserBuilder, Option) = + dsl::guild_members + .filter(dsl::guild_uuid.eq(guild_uuid)) + .filter(dsl::user_uuid.eq(user_uuid)) + .inner_join(users::table) + .left_join( + fdsl::friends.on(fdsl::uuid1 + .eq(me.uuid) + .and(fdsl::uuid2.eq(users::uuid)) + .or(fdsl::uuid2.eq(me.uuid).and(fdsl::uuid1.eq(users::uuid)))), + ) + .order_by(coalesce( + dsl::nickname, + users::display_name, + users::username, + )) + .select(( + MemberBuilder::as_select(), + UserBuilder::as_select(), + Option::::as_select(), + )) + .get_result(conn) + .await?; - member.build(conn, cache_pool, Some(me)).await + member + .build_with_parts(conn, cache_pool, user, friend) + .await } pub async fn fetch_one_with_member( @@ -137,35 +191,119 @@ impl Member { me: Option<&Me>, uuid: Uuid, ) -> Result { + let member: MemberBuilder; + let user: UserBuilder; + let friend: Option; + use friends::dsl as fdsl; use guild_members::dsl; - let member: MemberBuilder = dsl::guild_members - .filter(dsl::uuid.eq(uuid)) - .select(MemberBuilder::as_select()) - .get_result(conn) - .await?; + if let Some(me) = me { + (member, user, friend) = dsl::guild_members + .filter(dsl::uuid.eq(uuid)) + .inner_join(users::table) + .left_join( + fdsl::friends.on(fdsl::uuid1 + .eq(me.uuid) + .and(fdsl::uuid2.eq(users::uuid)) + .or(fdsl::uuid2.eq(me.uuid).and(fdsl::uuid1.eq(users::uuid)))), + ) + .order_by(coalesce( + dsl::nickname, + users::display_name, + users::username, + )) + .select(( + MemberBuilder::as_select(), + UserBuilder::as_select(), + Option::::as_select(), + )) + .get_result(conn) + .await?; + } else { + (member, user) = dsl::guild_members + .filter(dsl::uuid.eq(uuid)) + .inner_join(users::table) + .order_by(coalesce( + dsl::nickname, + users::display_name, + users::username, + )) + .select((MemberBuilder::as_select(), UserBuilder::as_select())) + .get_result(conn) + .await?; - member.build(conn, cache_pool, me).await + friend = None; + } + + member + .build_with_parts(conn, cache_pool, user, friend) + .await } - pub async fn fetch_all( + pub async fn fetch_page( conn: &mut Conn, cache_pool: &redis::Client, me: &Me, guild_uuid: Uuid, - ) -> Result, Error> { + pagination: PaginationRequest, + ) -> Result, Error> { + let per_page = pagination.per_page.unwrap_or(50); + let page_multiplier: i64 = ((pagination.page - 1) * per_page).into(); + + if !(10..=100).contains(&per_page) { + return Err(Error::BadRequest( + "Invalid amount per page requested".to_string(), + )); + } + + use friends::dsl as fdsl; use guild_members::dsl; - let member_builders: Vec = load_or_empty( + let member_builders: Vec<(MemberBuilder, UserBuilder, Option)> = load_or_empty( dsl::guild_members .filter(dsl::guild_uuid.eq(guild_uuid)) - .select(MemberBuilder::as_select()) + .inner_join(users::table) + .left_join( + fdsl::friends.on(fdsl::uuid1 + .eq(me.uuid) + .and(fdsl::uuid2.eq(users::uuid)) + .or(fdsl::uuid2.eq(me.uuid).and(fdsl::uuid1.eq(users::uuid)))), + ) + .limit(per_page.into()) + .offset(page_multiplier) + .order_by(coalesce( + dsl::nickname, + users::display_name, + users::username, + )) + .select(( + MemberBuilder::as_select(), + UserBuilder::as_select(), + Option::::as_select(), + )) .load(conn) .await, )?; - let mut members = vec![]; + let member_count: i64 = dsl::guild_members + .filter(dsl::guild_uuid.eq(guild_uuid)) + .count() + .get_result(conn) + .await?; - for builder in member_builders { - members.push(builder.build(conn, cache_pool, Some(me)).await?); + let pages = member_count as f32 / per_page as f32; + + let mut members = Pagination:: { + objects: Vec::with_capacity(member_builders.len()), + amount: member_builders.len() as i32, + pages: pages.ceil() as i32, + page: pagination.page, + }; + + for (member, user, friend) in member_builders { + members.objects.push( + member + .build_with_parts(conn, cache_pool, user, friend) + .await?, + ); } Ok(members) diff --git a/src/objects/mod.rs b/src/objects/mod.rs index 3bcce9c..5a013ca 100644 --- a/src/objects/mod.rs +++ b/src/objects/mod.rs @@ -4,7 +4,7 @@ use lettre::{ transport::smtp::authentication::Credentials, }; use log::debug; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use uuid::Uuid; mod bans; @@ -76,6 +76,20 @@ impl Cookies for Request { } */ +#[derive(Serialize)] +pub struct Pagination { + objects: Vec, + amount: i32, + pages: i32, + page: i32, +} + +#[derive(Deserialize)] +pub struct PaginationRequest { + pub page: i32, + pub per_page: Option, +} + fn load_or_empty( query_result: Result, diesel::result::Error>, ) -> Result, diesel::result::Error> { diff --git a/src/objects/user.rs b/src/objects/user.rs index a686c39..596a785 100644 --- a/src/objects/user.rs +++ b/src/objects/user.rs @@ -21,7 +21,7 @@ pub struct UserBuilder { } impl UserBuilder { - fn build(self) -> User { + pub fn build(self) -> User { User { uuid: self.uuid, username: self.username, @@ -36,13 +36,13 @@ impl UserBuilder { #[derive(Deserialize, Serialize, Clone)] pub struct User { - uuid: Uuid, + pub uuid: Uuid, username: String, display_name: Option, avatar: Option, pronouns: Option, about: Option, - friends_since: Option>, + pub friends_since: Option>, } impl User { From 8d91ec78a615ea834171880061cd6ab107fa060e Mon Sep 17 00:00:00 2001 From: Radical Date: Mon, 4 Aug 2025 22:55:48 +0200 Subject: [PATCH 155/160] refactor: rename fetch_one_with_member Renamed to fetch_one_with_uuid --- src/api/v1/members/uuid/ban.rs | 2 +- src/api/v1/members/uuid/mod.rs | 4 ++-- src/objects/member.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/api/v1/members/uuid/ban.rs b/src/api/v1/members/uuid/ban.rs index 888dca6..e828e69 100644 --- a/src/api/v1/members/uuid/ban.rs +++ b/src/api/v1/members/uuid/ban.rs @@ -34,7 +34,7 @@ pub async fn post( global_checks(&mut conn, &app_state.config, uuid).await?; let member = - Member::fetch_one_with_member(&mut conn, &app_state.cache_pool, None, member_uuid).await?; + Member::fetch_one_with_uuid(&mut conn, &app_state.cache_pool, None, member_uuid).await?; let caller = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; diff --git a/src/api/v1/members/uuid/mod.rs b/src/api/v1/members/uuid/mod.rs index 0832192..5bfd129 100644 --- a/src/api/v1/members/uuid/mod.rs +++ b/src/api/v1/members/uuid/mod.rs @@ -32,7 +32,7 @@ pub async fn get( let me = Me::get(&mut conn, uuid).await?; let member = - Member::fetch_one_with_member(&mut conn, &app_state.cache_pool, Some(&me), member_uuid) + Member::fetch_one_with_uuid(&mut conn, &app_state.cache_pool, Some(&me), member_uuid) .await?; Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; @@ -51,7 +51,7 @@ pub async fn delete( let me = Me::get(&mut conn, uuid).await?; let member = - Member::fetch_one_with_member(&mut conn, &app_state.cache_pool, Some(&me), member_uuid) + Member::fetch_one_with_uuid(&mut conn, &app_state.cache_pool, Some(&me), member_uuid) .await?; let deleter = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; diff --git a/src/objects/member.rs b/src/objects/member.rs index 59929cd..0bba11b 100644 --- a/src/objects/member.rs +++ b/src/objects/member.rs @@ -185,7 +185,7 @@ impl Member { .await } - pub async fn fetch_one_with_member( + pub async fn fetch_one_with_uuid( conn: &mut Conn, cache_pool: &redis::Client, me: Option<&Me>, From 642dbe5270cb72e418e77f9d784c6490daa41556 Mon Sep 17 00:00:00 2001 From: Radical Date: Tue, 5 Aug 2025 00:02:21 +0200 Subject: [PATCH 156/160] fix: remove order_by on single fetches --- src/objects/member.rs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/objects/member.rs b/src/objects/member.rs index 0bba11b..20109cf 100644 --- a/src/objects/member.rs +++ b/src/objects/member.rs @@ -167,11 +167,6 @@ impl Member { .and(fdsl::uuid2.eq(users::uuid)) .or(fdsl::uuid2.eq(me.uuid).and(fdsl::uuid1.eq(users::uuid)))), ) - .order_by(coalesce( - dsl::nickname, - users::display_name, - users::username, - )) .select(( MemberBuilder::as_select(), UserBuilder::as_select(), @@ -206,11 +201,6 @@ impl Member { .and(fdsl::uuid2.eq(users::uuid)) .or(fdsl::uuid2.eq(me.uuid).and(fdsl::uuid1.eq(users::uuid)))), ) - .order_by(coalesce( - dsl::nickname, - users::display_name, - users::username, - )) .select(( MemberBuilder::as_select(), UserBuilder::as_select(), @@ -222,11 +212,6 @@ impl Member { (member, user) = dsl::guild_members .filter(dsl::uuid.eq(uuid)) .inner_join(users::table) - .order_by(coalesce( - dsl::nickname, - users::display_name, - users::username, - )) .select((MemberBuilder::as_select(), UserBuilder::as_select())) .get_result(conn) .await?; From ac1678bfa8d98d757627f8b097684ee16aa8c07b Mon Sep 17 00:00:00 2001 From: Radical Date: Tue, 5 Aug 2025 00:02:30 +0200 Subject: [PATCH 157/160] fix: use dedicated function for member count --- src/objects/member.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/objects/member.rs b/src/objects/member.rs index 20109cf..1247ea2 100644 --- a/src/objects/member.rs +++ b/src/objects/member.rs @@ -268,13 +268,7 @@ impl Member { .await, )?; - let member_count: i64 = dsl::guild_members - .filter(dsl::guild_uuid.eq(guild_uuid)) - .count() - .get_result(conn) - .await?; - - let pages = member_count as f32 / per_page as f32; + let pages = Member::count(conn, guild_uuid).await? as f32 / per_page as f32; let mut members = Pagination:: { objects: Vec::with_capacity(member_builders.len()), From 8a7711cabc928a3a3c6b6a82fb22eda24cec2695 Mon Sep 17 00:00:00 2001 From: Radical Date: Tue, 5 Aug 2025 03:28:02 +0200 Subject: [PATCH 158/160] feat: Make me optional in Member::fetch_one() --- src/objects/member.rs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/objects/member.rs b/src/objects/member.rs index 1247ea2..f7e56da 100644 --- a/src/objects/member.rs +++ b/src/objects/member.rs @@ -107,7 +107,7 @@ impl MemberBuilder { } } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone)] pub struct Member { pub uuid: Uuid, pub nickname: Option, @@ -150,14 +150,17 @@ impl Member { pub async fn fetch_one( conn: &mut Conn, cache_pool: &redis::Client, - me: &Me, + me: Option<&Me>, user_uuid: Uuid, guild_uuid: Uuid, ) -> Result { + let member: MemberBuilder; + let user: UserBuilder; + let friend: Option; use friends::dsl as fdsl; use guild_members::dsl; - let (member, user, friend): (MemberBuilder, UserBuilder, Option) = - dsl::guild_members + if let Some(me) = me { + (member, user, friend) = dsl::guild_members .filter(dsl::guild_uuid.eq(guild_uuid)) .filter(dsl::user_uuid.eq(user_uuid)) .inner_join(users::table) @@ -174,6 +177,17 @@ impl Member { )) .get_result(conn) .await?; + } else { + (member, user) = dsl::guild_members + .filter(dsl::guild_uuid.eq(guild_uuid)) + .filter(dsl::user_uuid.eq(user_uuid)) + .inner_join(users::table) + .select((MemberBuilder::as_select(), UserBuilder::as_select())) + .get_result(conn) + .await?; + + friend = None; + } member .build_with_parts(conn, cache_pool, user, friend) From 5b24d0052bcf9a0d80898eeeb20f0a9ce0696069 Mon Sep 17 00:00:00 2001 From: Radical Date: Tue, 5 Aug 2025 03:28:24 +0200 Subject: [PATCH 159/160] feat: return member with messages --- src/objects/message.rs | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/objects/message.rs b/src/objects/message.rs index a5224e0..f30f14d 100644 --- a/src/objects/message.rs +++ b/src/objects/message.rs @@ -1,10 +1,15 @@ -use diesel::{Insertable, Queryable, Selectable}; +use diesel::{ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable}; +use diesel_async::RunQueryDsl; use serde::Serialize; use uuid::Uuid; -use crate::{Conn, error::Error, schema::messages}; +use crate::{ + Conn, + error::Error, + schema::{channels, guilds, messages}, +}; -use super::User; +use super::Member; #[derive(Clone, Queryable, Selectable, Insertable)] #[diesel(table_name = messages)] @@ -23,7 +28,16 @@ impl MessageBuilder { conn: &mut Conn, cache_pool: &redis::Client, ) -> Result { - let user = User::fetch_one(conn, cache_pool, self.user_uuid).await?; + use channels::dsl; + + let guild_uuid = dsl::channels + .filter(dsl::uuid.eq(self.channel_uuid)) + .inner_join(guilds::table) + .select(guilds::uuid) + .get_result(conn) + .await?; + + let member = Member::fetch_one(conn, cache_pool, None, self.user_uuid, guild_uuid).await?; Ok(Message { uuid: self.uuid, @@ -31,7 +45,7 @@ impl MessageBuilder { user_uuid: self.user_uuid, message: self.message.clone(), reply_to: self.reply_to, - user, + member, }) } } @@ -43,5 +57,5 @@ pub struct Message { user_uuid: Uuid, message: String, reply_to: Option, - user: User, + member: Member, } From 447c577a2a2ae3071f21333e76a4ac3d89995316 Mon Sep 17 00:00:00 2001 From: Radical Date: Tue, 5 Aug 2025 03:28:35 +0200 Subject: [PATCH 160/160] style: cargo clippy & fmt --- src/objects/me.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/objects/me.rs b/src/objects/me.rs index 0c54570..167e61e 100644 --- a/src/objects/me.rs +++ b/src/objects/me.rs @@ -284,7 +284,7 @@ impl Me { cache_pool: &redis::Client, new_status: i16, ) -> Result<(), Error> { - if new_status > 4 || new_status < 0 { + if !(0..=4).contains(&new_status) { return Err(Error::BadRequest("Invalid status code".to_string())); } self.online_status = new_status;