diff --git a/geneit_backend/Cargo.lock b/geneit_backend/Cargo.lock index 6e0443d..8f49901 100644 --- a/geneit_backend/Cargo.lock +++ b/geneit_backend/Cargo.lock @@ -83,6 +83,44 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "actix-multipart" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee489e3c01eae4d1c35b03c4493f71cb40d93f66b14558feb1b1a807671cc4e" +dependencies = [ + "actix-multipart-derive", + "actix-utils", + "actix-web", + "bytes", + "derive_more", + "futures-core", + "futures-util", + "httparse", + "local-waker", + "log", + "memchr", + "mime", + "serde", + "serde_json", + "serde_plain", + "tempfile", + "tokio", +] + +[[package]] +name = "actix-multipart-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ec592f234db8a253cf80531246a4407c8a70530423eea80688a6c5a44a110e7" +dependencies = [ + "darling 0.14.4", + "parse-size", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "actix-remote-ip" version = "0.1.0" @@ -700,14 +738,38 @@ dependencies = [ "memchr", ] +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core 0.14.4", + "darling_macro 0.14.4", +] + [[package]] name = "darling" version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.3", + "darling_macro 0.20.3", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", ] [[package]] @@ -724,13 +786,24 @@ dependencies = [ "syn 2.0.23", ] +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core 0.14.4", + "quote", + "syn 1.0.109", +] + [[package]] name = "darling_macro" version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ - "darling_core", + "darling_core 0.20.3", "quote", "syn 2.0.23", ] @@ -1057,6 +1130,7 @@ name = "geneit_backend" version = "0.1.0" dependencies = [ "actix-cors", + "actix-multipart", "actix-remote-ip", "actix-web", "anyhow", @@ -1701,6 +1775,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "parse-size" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "944553dd59c802559559161f9816429058b869003836120e262e8caec061b7ae" + [[package]] name = "paste" version = "1.0.13" @@ -2166,6 +2246,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_plain" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6018081315db179d0ce57b1fe4b62a12a0028c9cf9bbef868c9cf477b3c34ae" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2200,7 +2289,7 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea3cee93715c2e266b9338b7544da68a9f24e227722ba482bd1c024367c77c65" dependencies = [ - "darling", + "darling 0.20.3", "proc-macro2", "quote", "syn 2.0.23", diff --git a/geneit_backend/Cargo.toml b/geneit_backend/Cargo.toml index 3683a95..562be88 100644 --- a/geneit_backend/Cargo.toml +++ b/geneit_backend/Cargo.toml @@ -13,11 +13,12 @@ lazy_static = "1.4.0" anyhow = "1.0.71" actix-web = "4.3.1" actix-cors = "0.6.4" +actix-multipart = "0.6.0" +actix-remote-ip = "0.1.0" futures-util = "0.3.28" diesel = { version = "2.0.4", features = ["postgres"] } serde = { version = "1.0.163", features = ["derive"] } serde_json = "1.0.96" -actix-remote-ip = "0.1.0" mailchecker = "5.0.9" redis = "0.23.0" lettre = "0.10.4" @@ -27,4 +28,4 @@ light-openid = "1.0.1" thiserror = "1.0.40" serde_with = "3.1.0" rust_iso3166 = "0.1.10" -rust-s3 = "0.33.0" \ No newline at end of file +rust-s3 = "0.33.0" diff --git a/geneit_backend/migrations/2023-05-24-102711_create_users/down.sql b/geneit_backend/migrations/2023-05-24-102711_create_users/down.sql index 13f0fa5..449b5bd 100644 --- a/geneit_backend/migrations/2023-05-24-102711_create_users/down.sql +++ b/geneit_backend/migrations/2023-05-24-102711_create_users/down.sql @@ -2,6 +2,7 @@ DROP view if EXISTS families_memberships ; DROP table IF EXISTS couples; DROP table IF EXISTS members; +DROP table IF EXISTS photos; DROP table IF EXISTS memberships ; DROP table IF EXISTS families; DROP table IF EXISTS users; \ No newline at end of file diff --git a/geneit_backend/migrations/2023-05-24-102711_create_users/up.sql b/geneit_backend/migrations/2023-05-24-102711_create_users/up.sql index 75a1676..a7e0910 100644 --- a/geneit_backend/migrations/2023-05-24-102711_create_users/up.sql +++ b/geneit_backend/migrations/2023-05-24-102711_create_users/up.sql @@ -30,13 +30,22 @@ CREATE TABLE memberships ( PRIMARY KEY(user_id, family_id) ); +CREATE TABLE photos ( + id SERIAL PRIMARY KEY, + time_create VARCHAR(130) NOT NULL, + mime_type VARCHAR(150) NOT NULL, + sha512 VARCHAR(130) NOT NULL, + file_size INTEGER NOT NULL, + thumb_sha512 VARCHAR(130) NOT NULL +); + CREATE TABLE members ( id SERIAL PRIMARY KEY, family_id integer NOT NULL REFERENCES families, first_name VARCHAR(30) NULL, last_name VARCHAR(30) NULL, birth_last_name VARCHAR(30) NULL, - photo_id VARCHAR(255) NULL, + photo_id INTEGER NULL REFERENCES photos ON DELETE SET NULL, email VARCHAR(255) NULL, phone VARCHAR(30) NULL, address VARCHAR (155) NULL, @@ -60,7 +69,7 @@ CREATE TABLE members ( CREATE TABLE couples ( wife integer NOT NULL REFERENCES members, husband integer NOT NULL REFERENCES members, - photo_id VARCHAR(255) NULL, + photo_id INTEGER NULL REFERENCES photos ON DELETE SET NULL, wedding_year smallint NULL, wedding_month smallint NULL, wedding_day smallint NULL, diff --git a/geneit_backend/src/app_config.rs b/geneit_backend/src/app_config.rs index 5363d2b..24e1d9d 100644 --- a/geneit_backend/src/app_config.rs +++ b/geneit_backend/src/app_config.rs @@ -141,6 +141,10 @@ pub struct AppConfig { /// S3 skip auto create bucket if not existing #[arg(long, env)] pub s3_skip_auto_create_bucket: bool, + + /// Directory where temporary files are stored + #[arg(long, env, default_value = "/tmp")] + pub temp_dir: String, } lazy_static::lazy_static! { diff --git a/geneit_backend/src/constants.rs b/geneit_backend/src/constants.rs index 4ad4634..016d8a5 100644 --- a/geneit_backend/src/constants.rs +++ b/geneit_backend/src/constants.rs @@ -36,6 +36,13 @@ impl NumberValueConstraint { #[derive(Debug, Clone, serde::Serialize)] pub struct StaticConstraints { + pub date_year: NumberValueConstraint, + pub date_month: NumberValueConstraint, + pub date_day: NumberValueConstraint, + + pub photo_allowed_types: &'static [&'static str], + pub photo_max_size: usize, + pub mail_len: SizeConstraint, pub user_name_len: SizeConstraint, pub password_len: SizeConstraint, @@ -53,15 +60,18 @@ pub struct StaticConstraints { pub member_country: SizeConstraint, pub member_sex: SizeConstraint, pub member_note: SizeConstraint, - - pub date_year: NumberValueConstraint, - pub date_month: NumberValueConstraint, - pub date_day: NumberValueConstraint, } impl Default for StaticConstraints { fn default() -> Self { Self { + date_year: NumberValueConstraint::new(1, 2050), + date_month: NumberValueConstraint::new(1, 12), + date_day: NumberValueConstraint::new(1, 31), + + photo_allowed_types: &ALLOWED_PHOTOS_MIMETYPES, + photo_max_size: PHOTOS_MAX_SIZE, + mail_len: SizeConstraint::new(5, 255), user_name_len: SizeConstraint::new(3, 30), password_len: SizeConstraint::new(8, 255), @@ -81,9 +91,6 @@ impl Default for StaticConstraints { member_country: SizeConstraint::new(0, 2), member_sex: SizeConstraint::new(0, 1), member_note: SizeConstraint::new(0, 35000), - date_year: NumberValueConstraint::new(1, 2050), - date_month: NumberValueConstraint::new(1, 12), - date_day: NumberValueConstraint::new(1, 31), } } } @@ -108,3 +115,16 @@ pub const AUTH_TOKEN_MAX_INACTIVITY: Duration = Duration::from_secs(3600); /// Length of family invitation code pub const FAMILY_INVITATION_CODE_LEN: usize = 7; + +/// Allowed photos mimetypes +pub const ALLOWED_PHOTOS_MIMETYPES: [&str; 6] = [ + "image/jpeg", + "image/png", + "image/gif", + "image/bmp", + "image/webp", + "image/vnd.microsoft.icon", +]; + +/// Uploaded photos max size +pub const PHOTOS_MAX_SIZE: usize = 10 * 1000 * 1000; diff --git a/geneit_backend/src/controllers/members_controller.rs b/geneit_backend/src/controllers/members_controller.rs index ab0266d..7058cdb 100644 --- a/geneit_backend/src/controllers/members_controller.rs +++ b/geneit_backend/src/controllers/members_controller.rs @@ -5,6 +5,8 @@ use crate::extractors::member_extractor::FamilyAndMemberInPath; use crate::models::{Member, MemberID, Sex}; use crate::services::members_service; use crate::utils::countries_utils; +use actix_multipart::form::tempfile::TempFile; +use actix_multipart::form::MultipartForm; use actix_web::{web, HttpResponse}; serde_with::with_prefix!(prefix_birth "birth_"); @@ -292,3 +294,17 @@ pub async fn delete(m: FamilyAndMemberInPath) -> HttpResult { members_service::delete(&m).await?; Ok(HttpResponse::Ok().finish()) } + +#[derive(Debug, MultipartForm)] +pub struct UploadPhotoForm { + #[multipart(rename = "photo")] + photo: TempFile, +} + +/// Upload a new photo for a user +pub async fn set_photo( + _m: FamilyAndMemberInPath, + MultipartForm(_form): MultipartForm, +) -> HttpResult { + todo!() +} diff --git a/geneit_backend/src/main.rs b/geneit_backend/src/main.rs index 865c4e5..51c7222 100644 --- a/geneit_backend/src/main.rs +++ b/geneit_backend/src/main.rs @@ -1,4 +1,5 @@ use actix_cors::Cors; +use actix_multipart::form::tempfile::TempFileConfig; use actix_remote_ip::RemoteIPConfig; use actix_web::middleware::Logger; use actix_web::{web, App, HttpServer}; @@ -35,6 +36,8 @@ async fn main() -> std::io::Result<()> { .app_data(web::Data::new(RemoteIPConfig { proxy: AppConfig::get().proxy_ip.clone(), })) + // Uploaded files + .app_data(TempFileConfig::default().directory(&AppConfig::get().temp_dir)) // Config controller .route("/", web::get().to(server_controller::home)) .route( @@ -150,6 +153,10 @@ async fn main() -> std::io::Result<()> { "/family/{id}/member/{member_id}", web::delete().to(members_controller::delete), ) + .route( + "/family/{id}/member/{member_id}/photo", + web::put().to(members_controller::set_photo), + ) }) .bind(AppConfig::get().listen_address.as_str())? .run() diff --git a/geneit_backend/src/models.rs b/geneit_backend/src/models.rs index f31bc62..bdc3b64 100644 --- a/geneit_backend/src/models.rs +++ b/geneit_backend/src/models.rs @@ -117,6 +117,10 @@ pub struct FamilyMembership { pub count_admins: i64, } +/// Photo ID holder +#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)] +pub struct PhotoID(pub i32); + /// Member ID holder #[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)] pub struct MemberID(pub i32); @@ -153,7 +157,7 @@ pub struct Member { pub first_name: Option, pub last_name: Option, pub birth_last_name: Option, - pub photo_id: Option, + photo_id: Option, pub email: Option, pub phone: Option, pub address: Option, @@ -183,6 +187,10 @@ impl Member { FamilyID(self.family_id) } + pub fn photo_id(&self) -> Option { + self.photo_id.map(PhotoID) + } + pub fn sex(&self) -> Option { self.sex.as_deref().map(Sex::parse_str).unwrap_or_default() } diff --git a/geneit_backend/src/schema.rs b/geneit_backend/src/schema.rs index c2a0ba1..e8c490f 100644 --- a/geneit_backend/src/schema.rs +++ b/geneit_backend/src/schema.rs @@ -4,7 +4,7 @@ diesel::table! { couples (wife, husband) { wife -> Int4, husband -> Int4, - photo_id -> Nullable, + photo_id -> Nullable, wedding_year -> Nullable, wedding_month -> Nullable, wedding_day -> Nullable, @@ -30,7 +30,7 @@ diesel::table! { first_name -> Nullable, last_name -> Nullable, birth_last_name -> Nullable, - photo_id -> Nullable, + photo_id -> Nullable, email -> Nullable, phone -> Nullable, address -> Nullable, @@ -61,6 +61,17 @@ diesel::table! { } } +diesel::table! { + photos (id) { + id -> Int4, + time_create -> Varchar, + mime_type -> Varchar, + sha512 -> Varchar, + file_size -> Int4, + thumb_sha512 -> Varchar, + } +} + diesel::table! { users (id) { id -> Int4, @@ -78,8 +89,17 @@ diesel::table! { } } +diesel::joinable!(couples -> photos (photo_id)); diesel::joinable!(members -> families (family_id)); +diesel::joinable!(members -> photos (photo_id)); diesel::joinable!(memberships -> families (family_id)); diesel::joinable!(memberships -> users (user_id)); -diesel::allow_tables_to_appear_in_same_query!(couples, families, members, memberships, users,); +diesel::allow_tables_to_appear_in_same_query!( + couples, + families, + members, + memberships, + photos, + users, +); diff --git a/geneit_backend/src/services/members_service.rs b/geneit_backend/src/services/members_service.rs index 59214eb..1564542 100644 --- a/geneit_backend/src/services/members_service.rs +++ b/geneit_backend/src/services/members_service.rs @@ -60,7 +60,7 @@ pub async fn update(member: &mut Member) -> anyhow::Result<()> { members::dsl::first_name.eq(member.first_name.clone()), members::dsl::last_name.eq(member.last_name.clone()), members::dsl::birth_last_name.eq(member.birth_last_name.clone()), - members::dsl::photo_id.eq(member.photo_id.clone()), + members::dsl::photo_id.eq(member.photo_id().map(|p| p.0)), members::dsl::email.eq(member.email.clone()), members::dsl::phone.eq(member.phone.clone()), members::dsl::address.eq(member.address.clone()),