Can create a family member

This commit is contained in:
2023-08-04 19:03:46 +02:00
parent 274e9d9a21
commit f344765dd8
15 changed files with 1122 additions and 19 deletions

View File

@ -1,6 +1,6 @@
use std::time::Duration;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
pub struct SizeConstraint {
min: usize,
max: usize,
@ -17,6 +17,23 @@ impl SizeConstraint {
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct NumberValueConstraint {
min: i64,
max: i64,
}
impl NumberValueConstraint {
pub fn new(min: i64, max: i64) -> Self {
Self { min, max }
}
pub fn validate(&self, val: impl Into<i64>) -> bool {
let val = val.into();
val >= self.min && val <= self.max
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct StaticConstraints {
pub mail_len: SizeConstraint,
@ -24,6 +41,22 @@ pub struct StaticConstraints {
pub password_len: SizeConstraint,
pub family_name_len: SizeConstraint,
pub invitation_code_len: SizeConstraint,
pub member_first_name: SizeConstraint,
pub member_last_name: SizeConstraint,
pub member_birth_last_name: SizeConstraint,
pub member_email: SizeConstraint,
pub member_phone: SizeConstraint,
pub member_address: SizeConstraint,
pub member_city: SizeConstraint,
pub member_postal_code: SizeConstraint,
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 {
@ -37,6 +70,20 @@ impl Default for StaticConstraints {
FAMILY_INVITATION_CODE_LEN,
FAMILY_INVITATION_CODE_LEN,
),
member_first_name: SizeConstraint::new(0, 30),
member_last_name: SizeConstraint::new(0, 30),
member_birth_last_name: SizeConstraint::new(0, 30),
member_email: SizeConstraint::new(0, 255),
member_phone: SizeConstraint::new(0, 30),
member_address: SizeConstraint::new(0, 155),
member_city: SizeConstraint::new(0, 150),
member_postal_code: SizeConstraint::new(0, 12),
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),
}
}
}

View File

@ -0,0 +1,242 @@
use crate::constants::{SizeConstraint, StaticConstraints};
use crate::controllers::HttpResult;
use crate::extractors::family_extractor::FamilyInPath;
use crate::models::{Member, MemberID, Sex};
use crate::services::members_service;
use crate::utils::countries_utils;
use actix_web::{web, HttpResponse};
serde_with::with_prefix!(prefix_birth "birth_");
serde_with::with_prefix!(prefix_death "death_");
#[derive(serde::Deserialize)]
pub struct RequestDate {
pub year: Option<i16>,
pub month: Option<i16>,
pub day: Option<i16>,
}
impl RequestDate {
pub fn check(&self) -> bool {
let c = StaticConstraints::default();
self.year.map(|y| c.date_year.validate(y)).unwrap_or(true)
&& self.month.map(|y| c.date_month.validate(y)).unwrap_or(true)
&& self.day.map(|y| c.date_day.validate(y)).unwrap_or(true)
}
}
#[derive(serde::Deserialize)]
pub struct MemberRequest {
first_name: Option<String>,
last_name: Option<String>,
birth_last_name: Option<String>,
email: Option<String>,
phone: Option<String>,
address: Option<String>,
city: Option<String>,
postal_code: Option<String>,
country: Option<String>,
sex: Option<Sex>,
mother: Option<MemberID>,
father: Option<MemberID>,
#[serde(flatten, with = "prefix_birth")]
birth: Option<RequestDate>,
#[serde(flatten, with = "prefix_death")]
death: Option<RequestDate>,
note: Option<String>,
}
#[derive(thiserror::Error, Debug)]
enum MemberControllerErr {
#[error("Malformed first name!")]
MalformedFirstname,
#[error("Malformed last name!")]
MalformedLastname,
#[error("Malformed birth last name!")]
MalformedBirthLastname,
#[error("Malformed email address!")]
MalformedEmailAddress,
#[error("Invalid email address!")]
InvalidEmailAddress,
#[error("Malformed phone number!")]
MalformedPhoneNumber,
#[error("Malformed address!")]
MalformedAddress,
#[error("Malformed city!")]
MalformedCity,
#[error("Malformed postal code!")]
MalformedPostalCode,
#[error("Malformed country!")]
MalformedCountry,
#[error("Invalid country code!")]
InvalidCountryCode,
#[error("Malformed date of birth!")]
MalformedDateOfBirth,
#[error("Malformed date of death!")]
MalformedDateOfDeath,
#[error("Malformed note!")]
MalformedNote,
#[error("Mother does not exists!")]
MotherNotExisting,
#[error("Father does not exists!")]
FatherNotExisting,
}
fn check_opt_str_val(
val: &Option<String>,
c: SizeConstraint,
err: MemberControllerErr,
) -> anyhow::Result<()> {
if let Some(v) = val {
if !c.validate(v) {
return Err(err.into());
}
}
Ok(())
}
impl MemberRequest {
pub async fn to_member(self, member: &mut Member) -> anyhow::Result<()> {
let c = StaticConstraints::default();
check_opt_str_val(
&self.first_name,
c.member_first_name,
MemberControllerErr::MalformedFirstname,
)?;
check_opt_str_val(
&self.last_name,
c.member_last_name,
MemberControllerErr::MalformedLastname,
)?;
check_opt_str_val(
&self.birth_last_name,
c.member_birth_last_name,
MemberControllerErr::MalformedBirthLastname,
)?;
check_opt_str_val(
&self.email,
c.member_email,
MemberControllerErr::MalformedEmailAddress,
)?;
if let Some(mail) = &self.email {
if !mailchecker::is_valid(mail) {
return Err(MemberControllerErr::InvalidEmailAddress.into());
}
}
check_opt_str_val(
&self.phone,
c.member_phone,
MemberControllerErr::MalformedPhoneNumber,
)?;
check_opt_str_val(
&self.address,
c.member_address,
MemberControllerErr::MalformedAddress,
)?;
check_opt_str_val(
&self.city,
c.member_city,
MemberControllerErr::MalformedCity,
)?;
check_opt_str_val(
&self.postal_code,
c.member_postal_code,
MemberControllerErr::MalformedPostalCode,
)?;
check_opt_str_val(
&self.country,
c.member_country,
MemberControllerErr::MalformedCountry,
)?;
if let Some(c) = &self.country {
if !countries_utils::is_code_valid(c) {
return Err(MemberControllerErr::InvalidCountryCode.into());
}
}
if let Some(d) = &self.birth {
if !d.check() {
return Err(MemberControllerErr::MalformedDateOfBirth.into());
}
}
if let Some(d) = &self.death {
if !d.check() {
return Err(MemberControllerErr::MalformedDateOfDeath.into());
}
}
check_opt_str_val(
&self.note,
c.member_note,
MemberControllerErr::MalformedNote,
)?;
if let Some(mother) = self.mother {
if !members_service::exists(member.family_id(), mother).await? {
return Err(MemberControllerErr::MotherNotExisting.into());
}
}
if let Some(father) = self.father {
if !members_service::exists(member.family_id(), father).await? {
return Err(MemberControllerErr::FatherNotExisting.into());
}
}
member.first_name = self.first_name;
member.last_name = self.last_name;
member.birth_last_name = self.birth_last_name;
member.email = self.email;
member.phone = self.phone;
member.address = self.address;
member.city = self.city;
member.postal_code = self.postal_code;
member.country = self.country;
member.set_sex(self.sex);
member.set_mother(self.mother);
member.set_father(self.father);
member.birth_year = self.birth.as_ref().map(|m| m.year).unwrap_or_default();
member.birth_month = self.birth.as_ref().map(|m| m.month).unwrap_or_default();
member.birth_day = self.birth.as_ref().map(|m| m.day).unwrap_or_default();
member.death_year = self.death.as_ref().map(|m| m.year).unwrap_or_default();
member.death_month = self.death.as_ref().map(|m| m.month).unwrap_or_default();
member.death_day = self.death.as_ref().map(|m| m.day).unwrap_or_default();
member.note = self.note;
Ok(())
}
}
/// Create a new family member
pub async fn create(f: FamilyInPath, req: web::Json<MemberRequest>) -> HttpResult {
let mut member = members_service::create(f.family_id()).await?;
if let Err(e) = req.0.to_member(&mut member).await {
log::error!("Failed to apply member information! {e}");
members_service::delete(&member).await?;
return Ok(HttpResponse::BadRequest().body(e.to_string()));
}
if let Err(e) = members_service::update(&mut member).await {
log::error!("Failed to update member information! {e}");
members_service::delete(&member).await?;
return Ok(HttpResponse::InternalServerError().finish());
}
Ok(HttpResponse::Ok().json(member))
}

View File

@ -6,6 +6,7 @@ use std::fmt::{Debug, Display, Formatter};
pub mod auth_controller;
pub mod families_controller;
pub mod members_controller;
pub mod server_controller;
pub mod users_controller;

View File

@ -1,5 +1,7 @@
use crate::app_config::{AppConfig, OIDCProvider};
use crate::constants::StaticConstraints;
use crate::utils::countries_utils;
use crate::utils::countries_utils::CountryCode;
use actix_web::{HttpResponse, Responder};
/// Default hello route
@ -12,6 +14,7 @@ struct ServerConfig<'a> {
constraints: StaticConstraints,
mail: &'static str,
oidc_providers: Vec<OIDCProvider<'a>>,
countries: Vec<CountryCode>,
}
impl Default for ServerConfig<'_> {
@ -20,6 +23,7 @@ impl Default for ServerConfig<'_> {
mail: AppConfig::get().mail_sender.as_str(),
constraints: StaticConstraints::default(),
oidc_providers: AppConfig::get().openid_providers(),
countries: countries_utils::get_list(),
}
}
}

View File

@ -4,7 +4,7 @@ use actix_web::middleware::Logger;
use actix_web::{web, App, HttpServer};
use geneit_backend::app_config::AppConfig;
use geneit_backend::controllers::{
auth_controller, families_controller, server_controller, users_controller,
auth_controller, families_controller, members_controller, server_controller, users_controller,
};
#[actix_web::main]
@ -122,6 +122,11 @@ async fn main() -> std::io::Result<()> {
"/family/{id}/user/{user_id}",
web::delete().to(families_controller::delete_membership),
)
// Members controller
.route(
"/family/{id}/member/create",
web::post().to(members_controller::create),
)
})
.bind(AppConfig::get().listen_address.as_str())?
.run()

View File

@ -1,4 +1,4 @@
use crate::schema::{families, memberships, users};
use crate::schema::{families, members, memberships, users};
use diesel::prelude::*;
/// User ID holder
@ -116,3 +116,105 @@ pub struct FamilyMembership {
pub count_members: i64,
pub count_admins: i64,
}
/// Member ID holder
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub struct MemberID(pub i32);
#[derive(serde::Serialize, serde::Deserialize)]
pub enum Sex {
#[serde(rename = "M")]
Male,
#[serde(rename = "F")]
Female,
}
impl Sex {
pub fn from_str(s: &str) -> Option<Self> {
match s {
"M" => Some(Sex::Male),
"F" => Some(Sex::Female),
_ => None,
}
}
pub fn to_str(&self) -> &'static str {
match self {
Sex::Male => "M",
Sex::Female => "F",
}
}
}
#[derive(Queryable, Debug, serde::Serialize)]
pub struct Member {
id: i32,
family_id: i32,
pub first_name: Option<String>,
pub last_name: Option<String>,
pub birth_last_name: Option<String>,
pub photo_id: Option<String>,
pub email: Option<String>,
pub phone: Option<String>,
pub address: Option<String>,
pub city: Option<String>,
pub postal_code: Option<String>,
pub country: Option<String>,
sex: Option<String>,
time_create: i64,
pub time_update: i64,
mother: Option<i32>,
father: Option<i32>,
pub birth_year: Option<i16>,
pub birth_month: Option<i16>,
pub birth_day: Option<i16>,
pub death_year: Option<i16>,
pub death_month: Option<i16>,
pub death_day: Option<i16>,
pub note: Option<String>,
}
impl Member {
pub fn id(&self) -> MemberID {
MemberID(self.id)
}
pub fn family_id(&self) -> FamilyID {
FamilyID(self.family_id)
}
pub fn sex(&self) -> Option<Sex> {
self.sex
.as_deref()
.map(|s| Sex::from_str(s))
.unwrap_or_default()
}
pub fn set_sex(&mut self, s: Option<Sex>) {
self.sex = s.map(|s| s.to_str().to_string())
}
pub fn mother(&self) -> Option<MemberID> {
self.mother.map(MemberID)
}
pub fn set_mother(&mut self, p: Option<MemberID>) {
self.mother = p.map(|p| p.0);
}
pub fn father(&self) -> Option<MemberID> {
self.father.map(MemberID)
}
pub fn set_father(&mut self, p: Option<MemberID>) {
self.father = p.map(|p| p.0);
}
}
#[derive(Insertable)]
#[diesel(table_name = members)]
pub struct NewMember {
pub family_id: i32,
pub time_create: i64,
pub time_update: i64,
}

View File

@ -37,7 +37,7 @@ diesel::table! {
city -> Nullable<Varchar>,
postal_code -> Nullable<Varchar>,
country -> Nullable<Varchar>,
sex -> Varchar,
sex -> Nullable<Varchar>,
time_create -> Int8,
time_update -> Int8,
mother -> Nullable<Int4>,
@ -82,10 +82,4 @@ diesel::joinable!(members -> families (family_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, users,);

View File

@ -0,0 +1,91 @@
use crate::connections::db_connection;
use crate::models::{FamilyID, Member, MemberID, NewMember};
use crate::schema::members;
use crate::utils::time_utils::time;
use diesel::prelude::*;
use diesel::RunQueryDsl;
/// Create a new family member
pub async fn create(family_id: FamilyID) -> anyhow::Result<Member> {
db_connection::execute(|conn| {
let res: Member = diesel::insert_into(members::table)
.values(&NewMember {
family_id: family_id.0,
time_create: time() as i64,
time_update: time() as i64,
})
.get_result(conn)?;
Ok(res)
})
}
/// Get the information of a member
pub async fn get_by_id(id: MemberID) -> anyhow::Result<Member> {
db_connection::execute(|conn| members::table.filter(members::dsl::id.eq(id.0)).first(conn))
}
/// Check whether a member with a given id exists or not
pub async fn exists(family_id: FamilyID, member_id: MemberID) -> anyhow::Result<bool> {
db_connection::execute(|conn| {
let count: i64 = members::table
.filter(
members::id
.eq(member_id.0)
.and(members::family_id.eq(family_id.0)),
)
.count()
.get_result(conn)?;
Ok(count != 0)
})
}
/// Update the information of a member
pub async fn update(member: &mut Member) -> anyhow::Result<()> {
member.time_update = time() as i64;
db_connection::execute(|conn| {
diesel::update(members::dsl::members.filter(members::dsl::id.eq(member.id().0)))
.set((
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::email.eq(member.email.clone()),
members::dsl::phone.eq(member.phone.clone()),
members::dsl::address.eq(member.address.clone()),
members::dsl::city.eq(member.city.clone()),
members::dsl::postal_code.eq(member.postal_code.clone()),
members::dsl::country.eq(member.country.clone()),
members::dsl::sex.eq(member.sex().map(|s| s.to_str().to_string())),
members::dsl::time_update.eq(member.time_update),
members::dsl::mother.eq(member.mother().map(|m| m.0)),
members::dsl::father.eq(member.father().map(|m| m.0)),
members::dsl::birth_year.eq(member.birth_year),
members::dsl::birth_month.eq(member.birth_month),
members::dsl::birth_day.eq(member.birth_day),
members::dsl::death_year.eq(member.death_year),
members::dsl::death_month.eq(member.death_month),
members::dsl::death_day.eq(member.death_day),
members::dsl::note.eq(member.note.clone()),
))
.execute(conn)
})?;
Ok(())
}
/// Delete a member
pub async fn delete(member: &Member) -> anyhow::Result<()> {
// TODO : remove associated couple
// TODO : remove user photo
// Remove the member
db_connection::execute(|conn| {
diesel::delete(members::dsl::members.filter(members::dsl::id.eq(member.id().0)))
.execute(conn)
})?;
Ok(())
}

View File

@ -3,6 +3,7 @@
pub mod families_service;
pub mod login_token_service;
pub mod mail_service;
pub mod members_service;
pub mod openid_service;
pub mod rate_limiter_service;
pub mod users_service;

View File

@ -0,0 +1,48 @@
use lazy_static::lazy_static;
use std::collections::HashMap;
lazy_static! {
static ref COUNTRIES_FR: HashMap<String, String> =
serde_json::from_str(include_str!("../../assets/iso-3166_country_french.json")).unwrap();
}
pub fn is_code_valid(code: &str) -> bool {
rust_iso3166::ALL_ALPHA2.contains(&code)
}
#[derive(serde::Serialize, Debug, Copy, Clone)]
pub struct CountryCode {
code: &'static str,
en: &'static str,
fr: &'static str,
}
/// Get the entire list of countries
pub fn get_list() -> Vec<CountryCode> {
rust_iso3166::ALL
.iter()
.map(|c| CountryCode {
code: c.alpha2,
en: c.name,
fr: COUNTRIES_FR
.get(c.alpha2)
.map(|s| s.as_str())
.unwrap_or(c.name),
})
.collect()
}
#[cfg(test)]
mod test {
use crate::utils::countries_utils::is_code_valid;
#[test]
fn test_code_fr() {
assert!(is_code_valid("FR"))
}
#[test]
fn test_code_bad() {
assert!(!is_code_valid("ZZ"))
}
}

View File

@ -1,4 +1,5 @@
//! # App utilities
pub mod countries_utils;
pub mod string_utils;
pub mod time_utils;