Compare commits
3 commits
149b81973d
...
97072d54d1
Author | SHA1 | Date | |
---|---|---|---|
97072d54d1 | |||
d6364a0dc0 | |||
81f7527c79 |
6 changed files with 95 additions and 42 deletions
|
@ -36,6 +36,7 @@ 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"
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use actix_web::{get, patch, web, HttpRequest, HttpResponse};
|
||||
use actix_multipart::form::{json::Json as MpJson, tempfile::TempFile, MultipartForm};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{error::Error, structs::Me, api::v1::auth::check_access_token, utils::get_auth_header, Data};
|
||||
|
@ -18,7 +19,7 @@ pub async fn res(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse
|
|||
Ok(HttpResponse::Ok().json(me))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct NewInfo {
|
||||
username: Option<String>,
|
||||
display_name: Option<String>,
|
||||
|
@ -26,8 +27,15 @@ struct NewInfo {
|
|||
email: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, MultipartForm)]
|
||||
struct UploadForm {
|
||||
#[multipart(limit = "100MB")]
|
||||
avatar: Option<TempFile>,
|
||||
json: Option<MpJson<NewInfo>>,
|
||||
}
|
||||
|
||||
#[patch("/me")]
|
||||
pub async fn update(req: HttpRequest, new_info: web::Json<NewInfo>, data: web::Data<Data>) -> Result<HttpResponse, Error> {
|
||||
pub async fn update(req: HttpRequest, MultipartForm(form): MultipartForm<UploadForm>, data: web::Data<Data>) -> Result<HttpResponse, Error> {
|
||||
let headers = req.headers();
|
||||
|
||||
let auth_header = get_auth_header(headers)?;
|
||||
|
@ -36,22 +44,32 @@ pub async fn update(req: HttpRequest, new_info: web::Json<NewInfo>, data: web::D
|
|||
|
||||
let uuid = check_access_token(auth_header, &mut conn).await?;
|
||||
|
||||
let me = Me::get(&mut conn, uuid).await?;
|
||||
let mut me = Me::get(&mut conn, uuid).await?;
|
||||
|
||||
if let Some(username) = &new_info.username {
|
||||
todo!();
|
||||
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 Some(display_name) = &new_info.display_name {
|
||||
todo!();
|
||||
}
|
||||
if let Some(new_info) = form.json {
|
||||
if let Some(username) = &new_info.username {
|
||||
todo!();
|
||||
}
|
||||
|
||||
if let Some(password) = &new_info.password {
|
||||
todo!();
|
||||
}
|
||||
if let Some(display_name) = &new_info.display_name {
|
||||
todo!();
|
||||
}
|
||||
|
||||
if let Some(email) = &new_info.email {
|
||||
todo!();
|
||||
if let Some(password) = &new_info.password {
|
||||
todo!();
|
||||
}
|
||||
|
||||
if let Some(email) = &new_info.email {
|
||||
todo!();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
|
|
|
@ -9,6 +9,7 @@ pub fn web() -> Scope {
|
|||
web::scope("/users")
|
||||
.service(res)
|
||||
.service(me::res)
|
||||
.service(me::update)
|
||||
.service(uuid::res)
|
||||
}
|
||||
|
||||
|
|
|
@ -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::error;
|
||||
use log::{debug, error};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
|
@ -54,6 +54,7 @@ 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())
|
||||
|
|
|
@ -5,9 +5,8 @@ use diesel_async::{pooled_connection::AsyncDieselConnectionManager, RunQueryDsl}
|
|||
use tokio::task;
|
||||
use url::Url;
|
||||
use actix_web::web::BytesMut;
|
||||
use bindet::FileType;
|
||||
|
||||
use crate::{error::Error, Conn, Data, schema::*};
|
||||
use crate::{error::Error, schema::*, utils::image_check, Conn, Data};
|
||||
|
||||
fn load_or_empty<T>(query_result: Result<Vec<T>, diesel::result::Error>) -> Result<Vec<T>, diesel::result::Error> {
|
||||
match query_result {
|
||||
|
@ -256,7 +255,7 @@ impl GuildBuilder {
|
|||
uuid: self.uuid,
|
||||
name: self.name,
|
||||
description: self.description,
|
||||
icon: self.icon,
|
||||
icon: self.icon.and_then(|i| i.parse().ok()),
|
||||
owner_uuid: self.owner_uuid,
|
||||
roles: roles,
|
||||
member_count: member_count,
|
||||
|
@ -269,7 +268,7 @@ pub struct Guild {
|
|||
pub uuid: Uuid,
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
icon: Option<String>,
|
||||
icon: Option<Url>,
|
||||
owner_uuid: Uuid,
|
||||
pub roles: Vec<Role>,
|
||||
member_count: i64,
|
||||
|
@ -410,29 +409,13 @@ impl Guild {
|
|||
|
||||
// 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 ico = icon.clone();
|
||||
|
||||
let image_type = task::spawn_blocking(move || {
|
||||
let buf = std::io::Cursor::new(ico.to_vec());
|
||||
|
||||
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 String::from("jpg")
|
||||
} else if file_type.likely_to_be == vec![FileType::Png] {
|
||||
return String::from("png")
|
||||
}
|
||||
}
|
||||
String::from("unknown")
|
||||
}).await?;
|
||||
|
||||
if image_type == "unknown" {
|
||||
return Err(Error::BadRequest("Not an image".to_string()))
|
||||
}
|
||||
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.trim_start_matches("https://cdn.gorb.app/");
|
||||
let relative_url = icon
|
||||
.path()
|
||||
.trim_start_matches('/');
|
||||
|
||||
bunny_cdn.storage.delete(relative_url).await?;
|
||||
}
|
||||
|
@ -450,7 +433,7 @@ impl Guild {
|
|||
.execute(conn)
|
||||
.await?;
|
||||
|
||||
self.icon = Some(icon_url.to_string());
|
||||
self.icon = Some(icon_url);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -684,6 +667,38 @@ impl Me {
|
|||
|
||||
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)]
|
||||
|
|
21
src/utils.rs
21
src/utils.rs
|
@ -1,7 +1,8 @@
|
|||
use actix_web::{
|
||||
cookie::{Cookie, SameSite, time::Duration},
|
||||
http::header::HeaderMap,
|
||||
cookie::{time::Duration, Cookie, SameSite},
|
||||
http::header::HeaderMap, web::BytesMut,
|
||||
};
|
||||
use bindet::FileType;
|
||||
use getrandom::fill;
|
||||
use hex::encode;
|
||||
use redis::RedisError;
|
||||
|
@ -59,6 +60,22 @@ pub fn generate_refresh_token() -> Result<String, getrandom::Error> {
|
|||
Ok(encode(buf))
|
||||
}
|
||||
|
||||
pub fn image_check(icon: BytesMut) -> Result<String, Error> {
|
||||
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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue