diff --git a/Cargo.toml b/Cargo.toml index 568466d..8c03fec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,14 +29,11 @@ uuid = { version = "1.16", features = ["serde", "v7"] } random-string = "1.1" actix-ws = "0.3.0" futures-util = "0.3.31" -bunny-api-tokio = "0.2.1" -bindet = "0.3.2" deadpool = "0.12" diesel = { version = "2.2", features = ["uuid"] } 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" [dependencies.tokio] version = "1.44" diff --git a/migrations/2025-05-23-112318_add_guild_icons/down.sql b/migrations/2025-05-23-112318_add_guild_icons/down.sql deleted file mode 100644 index ac7eaf6..0000000 --- a/migrations/2025-05-23-112318_add_guild_icons/down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- This file should undo anything in `up.sql` -ALTER TABLE guilds DROP COLUMN icon; diff --git a/migrations/2025-05-23-112318_add_guild_icons/up.sql b/migrations/2025-05-23-112318_add_guild_icons/up.sql deleted file mode 100644 index f698e39..0000000 --- a/migrations/2025-05-23-112318_add_guild_icons/up.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Your SQL goes here -ALTER TABLE guilds ADD COLUMN icon VARCHAR(100) DEFAULT NULL; diff --git a/migrations/2025-05-23-113933_add_user_avatars/down.sql b/migrations/2025-05-23-113933_add_user_avatars/down.sql deleted file mode 100644 index 89b6268..0000000 --- a/migrations/2025-05-23-113933_add_user_avatars/down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- This file should undo anything in `up.sql` -ALTER TABLE users DROP COLUMN avatar; diff --git a/migrations/2025-05-23-113933_add_user_avatars/up.sql b/migrations/2025-05-23-113933_add_user_avatars/up.sql deleted file mode 100644 index 6c6a6af..0000000 --- a/migrations/2025-05-23-113933_add_user_avatars/up.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Your SQL goes here -ALTER TABLE users ADD COLUMN avatar varchar(100) DEFAULT NULL; diff --git a/src/api/v1/servers/uuid/icon.rs b/src/api/v1/servers/uuid/icon.rs deleted file mode 100644 index 297afa9..0000000 --- a/src/api/v1/servers/uuid/icon.rs +++ /dev/null @@ -1,36 +0,0 @@ -use actix_web::{put, web, HttpRequest, HttpResponse}; -use uuid::Uuid; -use futures_util::StreamExt as _; - -use crate::{error::Error, api::v1::auth::check_access_token, structs::{Guild, Member}, utils::get_auth_header, Data}; - -#[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?; - - Member::fetch_one(&mut conn, uuid, guild_uuid).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_cdn, &mut conn, data.config.bunny.cdn_url.clone(), bytes).await?; - - Ok(HttpResponse::Ok().finish()) -} diff --git a/src/api/v1/servers/uuid/mod.rs b/src/api/v1/servers/uuid/mod.rs index 47eff03..bac4004 100644 --- a/src/api/v1/servers/uuid/mod.rs +++ b/src/api/v1/servers/uuid/mod.rs @@ -4,7 +4,6 @@ use uuid::Uuid; mod channels; mod invites; mod roles; -mod icon; use crate::{ error::Error, @@ -32,8 +31,6 @@ pub fn web() -> Scope { // Invites .service(invites::get) .service(invites::create) - // Icon - .service(icon::upload) } #[get("/{uuid}")] diff --git a/src/api/v1/users/me.rs b/src/api/v1/users/me.rs index 9647002..49f88ba 100644 --- a/src/api/v1/users/me.rs +++ b/src/api/v1/users/me.rs @@ -1,8 +1,20 @@ -use actix_web::{get, patch, web, HttpRequest, HttpResponse}; -use actix_multipart::form::{json::Json as MpJson, tempfile::TempFile, MultipartForm}; -use serde::Deserialize; +use actix_web::{HttpRequest, HttpResponse, get, web}; +use diesel::{prelude::Queryable, ExpressionMethods, QueryDsl, Selectable, SelectableHelper}; +use diesel_async::RunQueryDsl; +use log::error; +use serde::Serialize; +use uuid::Uuid; -use crate::{error::Error, structs::Me, api::v1::auth::check_access_token, utils::get_auth_header, Data}; +use crate::{error::Error, api::v1::auth::check_access_token, schema::users::{self, dsl}, utils::get_auth_header, Data}; + +#[derive(Serialize, Queryable, Selectable)] +#[diesel(table_name = users)] +#[diesel(check_for_backend(diesel::pg::Pg))] +struct Response { + uuid: Uuid, + username: String, + display_name: Option, +} #[get("/me")] pub async fn res(req: HttpRequest, data: web::Data) -> Result { @@ -14,63 +26,16 @@ pub async fn res(req: HttpRequest, data: web::Data) -> Result = dsl::users + .filter(dsl::uuid.eq(uuid)) + .select(Response::as_select()) + .get_result(&mut conn) + .await; - Ok(HttpResponse::Ok().json(me)) -} - -#[derive(Debug, Deserialize)] -struct NewInfo { - username: Option, - display_name: Option, - password: Option, - email: Option, -} - -#[derive(Debug, MultipartForm)] -struct UploadForm { - #[multipart(limit = "100MB")] - avatar: Option, - json: Option>, -} - -#[patch("/me")] -pub async fn update(req: HttpRequest, MultipartForm(form): MultipartForm, 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 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.bunny_cdn, &mut conn, data.config.bunny.cdn_url.clone(), byte_slice.into()).await?; + if let Err(error) = user { + error!("{}", error); + return Ok(HttpResponse::InternalServerError().finish()) } - if let Some(new_info) = form.json { - if let Some(username) = &new_info.username { - todo!(); - } - - if let Some(display_name) = &new_info.display_name { - todo!(); - } - - if let Some(password) = &new_info.password { - todo!(); - } - - if let Some(email) = &new_info.email { - todo!(); - } - } - - Ok(HttpResponse::Ok().finish()) + Ok(HttpResponse::Ok().json(user.unwrap())) } diff --git a/src/api/v1/users/mod.rs b/src/api/v1/users/mod.rs index 57f5f7d..37d884a 100644 --- a/src/api/v1/users/mod.rs +++ b/src/api/v1/users/mod.rs @@ -1,15 +1,28 @@ use actix_web::{HttpRequest, HttpResponse, Scope, get, web}; +use diesel::{prelude::Queryable, QueryDsl, Selectable, SelectableHelper}; +use diesel_async::RunQueryDsl; +use serde::Serialize; +use ::uuid::Uuid; -use crate::{api::v1::auth::check_access_token, error::Error, structs::{StartAmountQuery, User}, utils::get_auth_header, Data}; +use crate::{error::Error,api::v1::auth::check_access_token, schema::users::{self, dsl}, structs::StartAmountQuery, utils::get_auth_header, Data}; mod me; mod uuid; +#[derive(Serialize, Queryable, Selectable)] +#[diesel(table_name = users)] +#[diesel(check_for_backend(diesel::pg::Pg))] +struct Response { + uuid: Uuid, + username: String, + display_name: Option, + email: String, +} + pub fn web() -> Scope { web::scope("/users") .service(res) .service(me::res) - .service(me::update) .service(uuid::res) } @@ -35,7 +48,13 @@ pub async fn res( check_access_token(auth_header, &mut conn).await?; - let users = User::fetch_amount(&mut conn, start, amount).await?; + let users: Vec = dsl::users + .order_by(dsl::username) + .offset(start) + .limit(amount) + .select(Response::as_select()) + .load(&mut conn) + .await?; Ok(HttpResponse::Ok().json(users)) } diff --git a/src/api/v1/users/uuid.rs b/src/api/v1/users/uuid.rs index 239524d..bfb0f69 100644 --- a/src/api/v1/users/uuid.rs +++ b/src/api/v1/users/uuid.rs @@ -1,8 +1,20 @@ use actix_web::{HttpRequest, HttpResponse, get, web}; +use diesel::{ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper}; +use diesel_async::RunQueryDsl; +use log::error; +use serde::Serialize; use uuid::Uuid; -use crate::{error::Error, api::v1::auth::check_access_token, structs::User, utils::get_auth_header, Data}; +use crate::{error::Error, api::v1::auth::check_access_token, schema::users::{self, dsl}, utils::get_auth_header, Data}; +#[derive(Serialize, Queryable, Selectable, Clone)] +#[diesel(table_name = users)] +#[diesel(check_for_backend(diesel::pg::Pg))] +struct Response { + uuid: Uuid, + username: String, + display_name: Option, +} #[get("/{uuid}")] pub async fn res( @@ -20,17 +32,28 @@ pub async fn res( check_access_token(auth_header, &mut conn).await?; - if let Ok(cache_hit) = data.get_cache_key(uuid.to_string()).await { + let cache_result = data.get_cache_key(uuid.to_string()).await; + + if let Ok(cache_hit) = cache_result { 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) + let user: Response = dsl::users + .filter(dsl::uuid.eq(uuid)) + .select(Response::as_select()) + .get_result(&mut conn) .await?; + let cache_result = data + .set_cache_key(uuid.to_string(), user.clone(), 1800) + .await; + + if let Err(error) = cache_result { + error!("{}", error); + return Ok(HttpResponse::InternalServerError().finish()); + } + Ok(HttpResponse::Ok().json(user)) } diff --git a/src/config.rs b/src/config.rs index 2d99818..3b537c9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,16 +1,13 @@ -use bunny_api_tokio::edge_storage::Endpoint; use crate::error::Error; use log::debug; use serde::Deserialize; use tokio::fs::read_to_string; -use url::Url; #[derive(Debug, Deserialize)] pub struct ConfigBuilder { database: Database, cache_database: CacheDatabase, web: Option, - bunny: BunnyBuilder, } #[derive(Debug, Deserialize, Clone)] @@ -38,14 +35,6 @@ struct WebBuilder { _ssl: Option, } -#[derive(Debug, Deserialize)] -struct BunnyBuilder { - api_key: String, - endpoint: String, - storage_zone: String, - cdn_url: Url, -} - impl ConfigBuilder { pub async fn load(path: String) -> Result { debug!("loading config from: {}", path); @@ -69,31 +58,10 @@ impl ConfigBuilder { } }; - 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, - }; - Config { database: self.database, cache_database: self.cache_database, web, - bunny, } } } @@ -103,7 +71,6 @@ pub struct Config { pub database: Database, pub cache_database: CacheDatabase, pub web: Web, - pub bunny: Bunny, } #[derive(Debug, Clone)] @@ -112,14 +79,6 @@ pub struct Web { pub port: u16, } -#[derive(Debug, Clone)] -pub struct Bunny { - pub api_key: String, - pub endpoint: Endpoint, - pub storage_zone: String, - pub cdn_url: Url, -} - impl Database { pub fn url(&self) -> String { let mut url = String::from("postgres://"); diff --git a/src/error.rs b/src/error.rs index 47415c0..5d10251 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,6 @@ use std::{io, time::SystemTimeError}; -use actix_web::{error::{PayloadError, ResponseError}, http::{header::{ContentType, ToStrError}, StatusCode}, HttpResponse}; +use actix_web::{error::ResponseError, http::{header::{ContentType, ToStrError}, StatusCode}, HttpResponse}; use deadpool::managed::{BuildError, PoolError}; use redis::RedisError; use serde::Serialize; @@ -10,7 +10,7 @@ use diesel_async::pooled_connection::PoolError as DieselPoolError; use tokio::task::JoinError; use serde_json::Error as JsonError; use toml::de::Error as TomlError; -use log::{debug, error}; +use log::error; #[derive(Debug, Error)] pub enum Error { @@ -38,12 +38,6 @@ pub enum Error { ToStrError(#[from] ToStrError), #[error(transparent)] RandomError(#[from] getrandom::Error), - #[error(transparent)] - BunnyError(#[from] bunny_api_tokio::error::Error), - #[error(transparent)] - UrlParseError(#[from] url::ParseError), - #[error(transparent)] - PayloadError(#[from] PayloadError), #[error("{0}")] PasswordHashError(String), #[error("{0}")] @@ -54,7 +48,6 @@ pub enum Error { impl ResponseError for Error { fn error_response(&self) -> HttpResponse { - debug!("{:?}", self); error!("{}: {}", self.status_code(), self.to_string()); HttpResponse::build(self.status_code()) diff --git a/src/main.rs b/src/main.rs index 8fa2121..7fb7087 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,10 +32,9 @@ struct Args { pub struct Data { pub pool: deadpool::managed::Pool, Conn>, pub cache_pool: redis::Client, - pub config: Config, + pub _config: Config, pub argon2: Argon2<'static>, pub start_time: SystemTime, - pub bunny_cdn: bunny_api_tokio::Client, } #[tokio::main] @@ -58,10 +57,6 @@ async fn main() -> Result<(), Error> { let cache_pool = redis::Client::open(config.cache_database.url())?; - let mut bunny_cdn = bunny_api_tokio::Client::new(config.bunny.api_key.clone()).await?; - - bunny_cdn.storage.init(config.bunny.endpoint.clone(), config.bunny.storage_zone.clone())?; - let database_url = config.database.url(); tokio::task::spawn_blocking(move || { @@ -95,11 +90,10 @@ async fn main() -> Result<(), Error> { let data = Data { pool, cache_pool, - config, + _config: config, // 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, }; HttpServer::new(move || { diff --git a/src/schema.rs b/src/schema.rs index 33935f7..b3274fc 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -48,8 +48,6 @@ diesel::table! { name -> Varchar, #[max_length = 300] description -> Nullable, - #[max_length = 100] - icon -> Nullable, } } @@ -123,8 +121,6 @@ diesel::table! { email_verified -> Bool, is_deleted -> Bool, deleted_at -> Nullable, - #[max_length = 100] - avatar -> Nullable, } } diff --git a/src/structs.rs b/src/structs.rs index e593996..3bc214b 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -1,12 +1,9 @@ -use diesel::{delete, insert_into, prelude::{Insertable, Queryable}, update, ExpressionMethods, QueryDsl, Selectable, SelectableHelper}; +use diesel::{delete, insert_into, prelude::{Insertable, Queryable}, ExpressionMethods, QueryDsl, Selectable, SelectableHelper}; use serde::{Deserialize, Serialize}; use uuid::Uuid; use diesel_async::{pooled_connection::AsyncDieselConnectionManager, RunQueryDsl}; -use tokio::task; -use url::Url; -use actix_web::web::BytesMut; -use crate::{error::Error, schema::*, utils::image_check, Conn, Data}; +use crate::{error::Error, Conn, Data, schema::*}; fn load_or_empty(query_result: Result, diesel::result::Error>) -> Result, diesel::result::Error> { match query_result { @@ -241,7 +238,6 @@ struct GuildBuilder { uuid: Uuid, name: String, description: Option, - icon: Option, owner_uuid: Uuid, } @@ -255,7 +251,7 @@ impl GuildBuilder { uuid: self.uuid, name: self.name, description: self.description, - icon: self.icon.and_then(|i| i.parse().ok()), + icon: String::from("bogus"), owner_uuid: self.owner_uuid, roles: roles, member_count: member_count, @@ -268,7 +264,7 @@ pub struct Guild { pub uuid: Uuid, name: String, description: Option, - icon: Option, + icon: String, owner_uuid: Uuid, pub roles: Vec, member_count: i64, @@ -327,7 +323,6 @@ impl Guild { uuid: guild_uuid, name: name.clone(), description: description.clone(), - icon: None, owner_uuid, }; @@ -354,7 +349,7 @@ impl Guild { uuid: guild_uuid, name, description, - icon: None, + icon: "bogus".to_string(), owner_uuid, roles: vec![], member_count: 1, @@ -406,37 +401,6 @@ impl Guild { 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)] @@ -607,100 +571,6 @@ impl Invite { } } -#[derive(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, -} - -impl User { - pub async fn fetch_one(conn: &mut Conn, user_uuid: Uuid) -> Result { - use users::dsl; - let user: User = dsl::users - .filter(dsl::uuid.eq(user_uuid)) - .select(User::as_select()) - .get_result(conn) - .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 { - uuid: Uuid, - username: String, - display_name: Option, - avatar: Option, - email: String, - 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 set_avatar(&mut self, bunny_cdn: &bunny_api_tokio::Client, conn: &mut Conn, cdn_url: Url, avatar: BytesMut) -> Result<(), Error> { - let avatar_clone = avatar.clone(); - let image_type = task::spawn_blocking(move || image_check(avatar_clone)).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?; - } - - let path = format!("avatar/{}/avatar.{}", self.uuid, image_type); - - 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(conn) - .await?; - - self.avatar = Some(avatar_url.to_string()); - - Ok(()) - } -} - #[derive(Deserialize)] pub struct StartAmountQuery { pub start: Option, diff --git a/src/utils.rs b/src/utils.rs index 4e9d435..b7ddcc1 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,8 +1,7 @@ use actix_web::{ - cookie::{time::Duration, Cookie, SameSite}, - http::header::HeaderMap, web::BytesMut, + cookie::{Cookie, SameSite, time::Duration}, + http::header::HeaderMap, }; -use bindet::FileType; use getrandom::fill; use hex::encode; use redis::RedisError; @@ -60,22 +59,6 @@ pub fn generate_refresh_token() -> Result { Ok(encode(buf)) } -pub fn image_check(icon: BytesMut) -> Result { - let buf = std::io::Cursor::new(icon); - - let detect = bindet::detect(buf).map_err(|e| e.kind()); - - if let Ok(Some(file_type)) = detect { - if file_type.likely_to_be == vec![FileType::Jpg] { - return Ok(String::from("jpg")) - } else if file_type.likely_to_be == vec![FileType::Png] { - return Ok(String::from("png")) - } - } - - Err(Error::BadRequest("Uploaded file is not an image".to_string())) -} - impl Data { pub async fn set_cache_key( &self,