mirror of
				https://github.com/nitnelave/lldap.git
				synced 2023-04-12 14:25:13 +00:00 
			
		
		
		
	server: Add support for users' avatars in GrahpQL
This commit is contained in:
		
							parent
							
								
									f29c88842a
								
							
						
					
					
						commit
						61eb71ebd3
					
				
							
								
								
									
										11
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										11
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							@ -2115,6 +2115,7 @@ dependencies = [
 | 
			
		||||
 "futures-util",
 | 
			
		||||
 "hmac 0.10.1",
 | 
			
		||||
 "http",
 | 
			
		||||
 "image",
 | 
			
		||||
 "itertools",
 | 
			
		||||
 "juniper",
 | 
			
		||||
 "juniper_actix",
 | 
			
		||||
@ -2133,6 +2134,7 @@ dependencies = [
 | 
			
		||||
 "sea-query-binder",
 | 
			
		||||
 "secstr",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_bytes",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 "sha2",
 | 
			
		||||
 "sqlx",
 | 
			
		||||
@ -3317,6 +3319,15 @@ dependencies = [
 | 
			
		||||
 "serde_derive",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "serde_bytes"
 | 
			
		||||
version = "0.11.7"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "cfc50e8183eeeb6178dcb167ae34a8051d63535023ae38b5d8d12beae193d37b"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "serde",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "serde_derive"
 | 
			
		||||
version = "1.0.137"
 | 
			
		||||
 | 
			
		||||
@ -90,6 +90,7 @@ impl CommonComponent<CreateUserForm> for CreateUserForm {
 | 
			
		||||
                        displayName: to_option(model.display_name),
 | 
			
		||||
                        firstName: to_option(model.first_name),
 | 
			
		||||
                        lastName: to_option(model.last_name),
 | 
			
		||||
                        avatar: None,
 | 
			
		||||
                    },
 | 
			
		||||
                };
 | 
			
		||||
                self.common.call_graphql::<CreateUser, _>(
 | 
			
		||||
 | 
			
		||||
@ -234,6 +234,7 @@ impl UserDetailsForm {
 | 
			
		||||
            displayName: None,
 | 
			
		||||
            firstName: None,
 | 
			
		||||
            lastName: None,
 | 
			
		||||
            avatar: None,
 | 
			
		||||
        };
 | 
			
		||||
        let default_user_input = user_input.clone();
 | 
			
		||||
        let model = self.form.model();
 | 
			
		||||
 | 
			
		||||
@ -85,6 +85,7 @@ impl User {
 | 
			
		||||
                display_name,
 | 
			
		||||
                first_name,
 | 
			
		||||
                last_name,
 | 
			
		||||
                avatar: None,
 | 
			
		||||
            },
 | 
			
		||||
            password,
 | 
			
		||||
            dn,
 | 
			
		||||
 | 
			
		||||
@ -60,6 +60,7 @@ input CreateUserInput {
 | 
			
		||||
  displayName: String
 | 
			
		||||
  firstName: String
 | 
			
		||||
  lastName: String
 | 
			
		||||
  avatar: String
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type User {
 | 
			
		||||
@ -68,6 +69,7 @@ type User {
 | 
			
		||||
  displayName: String!
 | 
			
		||||
  firstName: String!
 | 
			
		||||
  lastName: String!
 | 
			
		||||
  avatar: String!
 | 
			
		||||
  creationDate: DateTimeUtc!
 | 
			
		||||
  uuid: String!
 | 
			
		||||
  "The groups to which this user belongs."
 | 
			
		||||
@ -85,6 +87,7 @@ input UpdateUserInput {
 | 
			
		||||
  displayName: String
 | 
			
		||||
  firstName: String
 | 
			
		||||
  lastName: String
 | 
			
		||||
  avatar: String
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
schema {
 | 
			
		||||
 | 
			
		||||
@ -45,6 +45,7 @@ tracing-actix-web = "0.4.0-beta.7"
 | 
			
		||||
tracing-attributes = "^0.1.21"
 | 
			
		||||
tracing-log = "*"
 | 
			
		||||
rustls-pemfile = "1.0.0"
 | 
			
		||||
serde_bytes = "0.11.7"
 | 
			
		||||
 | 
			
		||||
[dependencies.chrono]
 | 
			
		||||
features = ["serde"]
 | 
			
		||||
@ -117,5 +118,10 @@ version = "^0.1.4"
 | 
			
		||||
features = ["default", "rustls"]
 | 
			
		||||
version = "=3.0.0-beta.5"
 | 
			
		||||
 | 
			
		||||
[dependencies.image]
 | 
			
		||||
features = ["jpeg"]
 | 
			
		||||
default-features = false
 | 
			
		||||
version = "0.24"
 | 
			
		||||
 | 
			
		||||
[dev-dependencies]
 | 
			
		||||
mockall = "0.9.1"
 | 
			
		||||
 | 
			
		||||
@ -82,6 +82,69 @@ impl From<String> for UserId {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(PartialEq, Eq, Clone, Debug, Default, Serialize, Deserialize, sqlx::Type)]
 | 
			
		||||
#[sqlx(transparent)]
 | 
			
		||||
pub struct JpegPhoto(#[serde(with = "serde_bytes")] Vec<u8>);
 | 
			
		||||
 | 
			
		||||
impl From<JpegPhoto> for sea_query::Value {
 | 
			
		||||
    fn from(photo: JpegPhoto) -> Self {
 | 
			
		||||
        photo.0.into()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<&JpegPhoto> for sea_query::Value {
 | 
			
		||||
    fn from(photo: &JpegPhoto) -> Self {
 | 
			
		||||
        photo.0.as_slice().into()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl TryFrom<Vec<u8>> for JpegPhoto {
 | 
			
		||||
    type Error = anyhow::Error;
 | 
			
		||||
    fn try_from(bytes: Vec<u8>) -> anyhow::Result<Self> {
 | 
			
		||||
        // Confirm that it's a valid Jpeg, then store only the bytes.
 | 
			
		||||
        image::io::Reader::with_format(
 | 
			
		||||
            std::io::Cursor::new(bytes.as_slice()),
 | 
			
		||||
            image::ImageFormat::Jpeg,
 | 
			
		||||
        )
 | 
			
		||||
        .decode()?;
 | 
			
		||||
        Ok(JpegPhoto(bytes))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl TryFrom<String> for JpegPhoto {
 | 
			
		||||
    type Error = anyhow::Error;
 | 
			
		||||
    fn try_from(string: String) -> anyhow::Result<Self> {
 | 
			
		||||
        // The String format is in base64.
 | 
			
		||||
        Self::try_from(base64::decode(string.as_str())?)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<&JpegPhoto> for String {
 | 
			
		||||
    fn from(val: &JpegPhoto) -> Self {
 | 
			
		||||
        base64::encode(&val.0)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl JpegPhoto {
 | 
			
		||||
    pub fn for_tests() -> Self {
 | 
			
		||||
        use image::{ImageOutputFormat, Rgb, RgbImage};
 | 
			
		||||
        let img = RgbImage::from_fn(32, 32, |x, y| {
 | 
			
		||||
            if (x + y) % 2 == 0 {
 | 
			
		||||
                Rgb([0, 0, 0])
 | 
			
		||||
            } else {
 | 
			
		||||
                Rgb([255, 255, 255])
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        let mut bytes: Vec<u8> = Vec::new();
 | 
			
		||||
        img.write_to(
 | 
			
		||||
            &mut std::io::Cursor::new(&mut bytes),
 | 
			
		||||
            ImageOutputFormat::Jpeg(0),
 | 
			
		||||
        )
 | 
			
		||||
        .unwrap();
 | 
			
		||||
        Self(bytes)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
 | 
			
		||||
pub struct User {
 | 
			
		||||
    pub user_id: UserId,
 | 
			
		||||
@ -89,7 +152,7 @@ pub struct User {
 | 
			
		||||
    pub display_name: String,
 | 
			
		||||
    pub first_name: String,
 | 
			
		||||
    pub last_name: String,
 | 
			
		||||
    // pub avatar: ?,
 | 
			
		||||
    pub avatar: JpegPhoto,
 | 
			
		||||
    pub creation_date: chrono::DateTime<chrono::Utc>,
 | 
			
		||||
    pub uuid: Uuid,
 | 
			
		||||
}
 | 
			
		||||
@ -105,6 +168,7 @@ impl Default for User {
 | 
			
		||||
            display_name: String::new(),
 | 
			
		||||
            first_name: String::new(),
 | 
			
		||||
            last_name: String::new(),
 | 
			
		||||
            avatar: JpegPhoto::default(),
 | 
			
		||||
            creation_date: epoch,
 | 
			
		||||
            uuid: Uuid::from_name_and_date("", &epoch),
 | 
			
		||||
        }
 | 
			
		||||
@ -159,6 +223,7 @@ pub struct CreateUserRequest {
 | 
			
		||||
    pub display_name: Option<String>,
 | 
			
		||||
    pub first_name: Option<String>,
 | 
			
		||||
    pub last_name: Option<String>,
 | 
			
		||||
    pub avatar: Option<JpegPhoto>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone, Default)]
 | 
			
		||||
@ -169,6 +234,7 @@ pub struct UpdateUserRequest {
 | 
			
		||||
    pub display_name: Option<String>,
 | 
			
		||||
    pub first_name: Option<String>,
 | 
			
		||||
    pub last_name: Option<String>,
 | 
			
		||||
    pub avatar: Option<JpegPhoto>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
 | 
			
		||||
@ -263,4 +329,11 @@ mod tests {
 | 
			
		||||
            Uuid::from_name_and_date(user_id, &date2)
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_jpeg_try_from_bytes() {
 | 
			
		||||
        let base64_raw = "/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAP//////////////////////////////////////////////////////////////////////////////////////2wBDAf//////////////////////////////////////////////////////////////////////////////////////wAARCADqATkDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAECA//EACQQAQEBAAIBBAMBAQEBAAAAAAABESExQQISUXFhgZGxocHw/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAH/xAAWEQEBAQAAAAAAAAAAAAAAAAAAEQH/2gAMAwEAAhEDEQA/AMriLyCKgg1gQwCgs4FTMOdutepjQak+FzMSVqgxZdRdPPIIvH5WzzGdBriphtTeAXg2ZjKA1pqKDUGZca3foBek8gFv8Ie3fKdA1qb8s7hoL6eLVt51FsAnql3Ut1M7AWbflLMDkEMX/F6/YjK/pADFQAUNA6alYagKk72m/j9p4Bq2fDDSYKLNXPNLoHE/NT6RYC31cJxZ3yWVM+aBYi/S2ZgiAsnYJx5D21vPmqrm3PTfpQQwyAC8JZvSKDni41ZrMuUVVl+Uz9w9v/1QWrZsZ5nFPHYH+JZyureQSF5M+fJ0CAfwRAVRBQA1DAWVUayoJUWoDpsxntPsueBV4+VxhdyAtv8AjOLGpIDMLbeGvbF4iozJfr/WukAVABAXAQXEAAASzVAZdO2WNordm+emFl7XcQSNZiFtv0C9w90nhJf4mA1u+GcJFwIyAqL/AOovwgGNfSRqdIrNa29M0gKCAojU9PAMjWXpckEJFNFEAAXEUBABYz6rZ0ureQc9vyt9XxDF2QAXtABcQAs0AZywkvluJbyipifas52DcyxjlZweAO0xri/hc+wZOEKIu6nSyeToVZyWXwvCg53gW81QQ7aTNAn5dGZJPs1UXURQAUEMCXQLZE93PRZ5hPTgNMrbIzKCm52LZwCs+2M8w2g3sjPuZAXb4IsMAUACzVUGM4/K+md6vEXUUyM5PDR0IxYe6ramih0VNBrS4xoqN8Q1BFQk3yqyAsioioAAKgDSJL4/jQIn5igLrPqtOuf6oOaxbMoAltUAhhIoJiiggrPu+AaOIxtAX3JbaAIaLwi4t9X4T3fg2AFtqcrUUarP20zUDAmqoE0WRBZPNVUVEAAAAVAC8kvih2DSKxOdBqs7Z0l0gI0mKAC4AuHE7ZtBriM+744QAAAAABAFsveIttBICyaikvy1+r/Cen5rWQHIBQa4rIDRqSl5qDWqziqgAAAATA7BpGdqXb2C2+J/UgAtRQBSQtkBWb6vhLbQAAAAAEBRAAAAAUbm+GZNdPxAP+ql2Tjwx7/wIgZ8iKvBk+CJoCXii9gaqZ/qqihAAAEVABGkBFUwBftNkZ3QW34QAAABFAQAVAAAAAARVkl8gs/43sk1jL45LvHArepk+E9XTG35oLqsmIKmLAEygKg0y1AFQBUXwgAAAoBC34S3UAAABAVAAAAAABAUQAVABdRQa1PcYyit2z58M8C4ouM2NXpOEGeWtNZUatiAIoAKIoCoAoG4C9MW6dgIoAIAAAAAAACKWAgL0CAAAALiANCKioNLgM1CrLihmTafkt1EF3SZ5ZVUW4mnIKvAi5fhEURVDWVQBRAAAAAAAAQFRVyAyulgAqCKlF8IqLsEgC9mGoC+IusqCrv5ZEUVOk1RuJfwSLOOkGFi4XPCoYYrNiKauosBGi9ICstM1UAAAAAAFQ0VcTBAXUGgIqGoKhKAzRRUQUAwxoSrGRpkQA/qiosOL9oJptMRRVZa0VUqSiChE6BqMgCwqKqIogAIAqKCKgKoogg0lBFuIKgAAAKNRlf2gqsftsEtZWoAAqAACKoMqAAeSoqp39kL2AqLOlE8rEBFQARYALhigrNC9gGmooLp4TweEQFFBFAECgIoAu0ifIAqAAA//9k=";
 | 
			
		||||
        let base64_jpeg = base64::decode(base64_raw).unwrap();
 | 
			
		||||
        JpegPhoto::try_from(base64_jpeg).unwrap();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -367,6 +367,7 @@ impl BackendHandler for SqlBackendHandler {
 | 
			
		||||
            Users::DisplayName,
 | 
			
		||||
            Users::FirstName,
 | 
			
		||||
            Users::LastName,
 | 
			
		||||
            Users::Avatar,
 | 
			
		||||
            Users::CreationDate,
 | 
			
		||||
            Users::Uuid,
 | 
			
		||||
        ];
 | 
			
		||||
@ -378,6 +379,7 @@ impl BackendHandler for SqlBackendHandler {
 | 
			
		||||
            request.display_name.unwrap_or_default().into(),
 | 
			
		||||
            request.first_name.unwrap_or_default().into(),
 | 
			
		||||
            request.last_name.unwrap_or_default().into(),
 | 
			
		||||
            request.avatar.unwrap_or_default().into(),
 | 
			
		||||
            now.naive_utc().into(),
 | 
			
		||||
            uuid.into(),
 | 
			
		||||
        ];
 | 
			
		||||
@ -409,6 +411,9 @@ impl BackendHandler for SqlBackendHandler {
 | 
			
		||||
        if let Some(last_name) = request.last_name {
 | 
			
		||||
            values.push((Users::LastName, last_name.into()));
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(avatar) = request.avatar {
 | 
			
		||||
            values.push((Users::Avatar, avatar.into()));
 | 
			
		||||
        }
 | 
			
		||||
        if values.is_empty() {
 | 
			
		||||
            return Ok(());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,8 @@
 | 
			
		||||
use crate::domain::handler::{
 | 
			
		||||
    BackendHandler, CreateUserRequest, GroupId, UpdateGroupRequest, UpdateUserRequest, UserId,
 | 
			
		||||
    BackendHandler, CreateUserRequest, GroupId, JpegPhoto, UpdateGroupRequest, UpdateUserRequest,
 | 
			
		||||
    UserId,
 | 
			
		||||
};
 | 
			
		||||
use anyhow::Context as AnyhowContext;
 | 
			
		||||
use juniper::{graphql_object, FieldResult, GraphQLInputObject, GraphQLObject};
 | 
			
		||||
use tracing::{debug, debug_span, Instrument};
 | 
			
		||||
 | 
			
		||||
@ -28,6 +30,8 @@ pub struct CreateUserInput {
 | 
			
		||||
    display_name: Option<String>,
 | 
			
		||||
    first_name: Option<String>,
 | 
			
		||||
    last_name: Option<String>,
 | 
			
		||||
    // Base64 encoded JpegPhoto.
 | 
			
		||||
    avatar: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(PartialEq, Eq, Debug, GraphQLInputObject)]
 | 
			
		||||
@ -38,6 +42,8 @@ pub struct UpdateUserInput {
 | 
			
		||||
    display_name: Option<String>,
 | 
			
		||||
    first_name: Option<String>,
 | 
			
		||||
    last_name: Option<String>,
 | 
			
		||||
    // Base64 encoded JpegPhoto.
 | 
			
		||||
    avatar: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(PartialEq, Eq, Debug, GraphQLInputObject)]
 | 
			
		||||
@ -73,6 +79,14 @@ impl<Handler: BackendHandler + Sync> Mutation<Handler> {
 | 
			
		||||
            return Err("Unauthorized user creation".into());
 | 
			
		||||
        }
 | 
			
		||||
        let user_id = UserId::new(&user.id);
 | 
			
		||||
        let avatar = user
 | 
			
		||||
            .avatar
 | 
			
		||||
            .map(base64::decode)
 | 
			
		||||
            .transpose()
 | 
			
		||||
            .context("Invalid base64 image")?
 | 
			
		||||
            .map(JpegPhoto::try_from)
 | 
			
		||||
            .transpose()
 | 
			
		||||
            .context("Provided image is not a valid JPEG")?;
 | 
			
		||||
        context
 | 
			
		||||
            .handler
 | 
			
		||||
            .create_user(CreateUserRequest {
 | 
			
		||||
@ -81,6 +95,7 @@ impl<Handler: BackendHandler + Sync> Mutation<Handler> {
 | 
			
		||||
                display_name: user.display_name,
 | 
			
		||||
                first_name: user.first_name,
 | 
			
		||||
                last_name: user.last_name,
 | 
			
		||||
                avatar,
 | 
			
		||||
            })
 | 
			
		||||
            .instrument(span.clone())
 | 
			
		||||
            .await?;
 | 
			
		||||
@ -126,6 +141,14 @@ impl<Handler: BackendHandler + Sync> Mutation<Handler> {
 | 
			
		||||
            span.in_scope(|| debug!("Unauthorized"));
 | 
			
		||||
            return Err("Unauthorized user update".into());
 | 
			
		||||
        }
 | 
			
		||||
        let avatar = user
 | 
			
		||||
            .avatar
 | 
			
		||||
            .map(base64::decode)
 | 
			
		||||
            .transpose()
 | 
			
		||||
            .context("Invalid base64 image")?
 | 
			
		||||
            .map(JpegPhoto::try_from)
 | 
			
		||||
            .transpose()
 | 
			
		||||
            .context("Provided image is not a valid JPEG")?;
 | 
			
		||||
        context
 | 
			
		||||
            .handler
 | 
			
		||||
            .update_user(UpdateUserRequest {
 | 
			
		||||
@ -134,6 +157,7 @@ impl<Handler: BackendHandler + Sync> Mutation<Handler> {
 | 
			
		||||
                display_name: user.display_name,
 | 
			
		||||
                first_name: user.first_name,
 | 
			
		||||
                last_name: user.last_name,
 | 
			
		||||
                avatar,
 | 
			
		||||
            })
 | 
			
		||||
            .instrument(span)
 | 
			
		||||
            .await?;
 | 
			
		||||
 | 
			
		||||
@ -217,6 +217,10 @@ impl<Handler: BackendHandler + Sync> User<Handler> {
 | 
			
		||||
        &self.user.last_name
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn avatar(&self) -> String {
 | 
			
		||||
        (&self.user.avatar).into()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn creation_date(&self) -> chrono::DateTime<chrono::Utc> {
 | 
			
		||||
        self.user.creation_date
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1459,6 +1459,7 @@ mod tests {
 | 
			
		||||
                        display_name: "Jimminy Cricket".to_string(),
 | 
			
		||||
                        first_name: "Jim".to_string(),
 | 
			
		||||
                        last_name: "Cricket".to_string(),
 | 
			
		||||
                        avatar: JpegPhoto::for_tests(),
 | 
			
		||||
                        uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
 | 
			
		||||
                        creation_date: Utc.ymd(2014, 7, 8).and_hms(9, 10, 11),
 | 
			
		||||
                    },
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user