mirror of
https://github.com/lldap/lldap.git
synced 2024-11-25 09:06:03 +00:00
server: Add support for users' avatars in GrahpQL
This commit is contained in:
parent
0e3c5120da
commit
3acc448048
11 changed files with 132 additions and 2 deletions
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 a new issue