feat(user-avatar): users can now edit there image (image is taken from OIDC picture)

This commit is contained in:
Edouard Vanbelle
2026-05-26 00:50:38 +02:00
parent 8b6c8e345b
commit b0c5e7827e
43 changed files with 1563 additions and 250 deletions
+4
View File
@@ -17,6 +17,8 @@ pub struct UserDto {
pub last_login_at: Option<DateTime<Utc>>,
pub active: bool,
pub auth_provider: String,
pub image: Option<String>,
pub can_edit_image: bool,
}
impl From<User> for UserDto {
@@ -33,6 +35,8 @@ impl From<User> for UserDto {
last_login_at: user.last_login_at(),
active: user.is_active(),
auth_provider: user.oidc_provider().unwrap_or("local").to_string(),
image: user.image().map(|s| s.to_string()),
can_edit_image: !user.is_oidc_user(),
}
}
}
+1
View File
@@ -151,6 +151,7 @@ pub struct OidcIdClaims {
pub preferred_username: Option<String>,
pub name: Option<String>,
pub groups: Vec<String>,
pub picture: Option<String>,
}
/// Port for OIDC operations — implemented in infrastructure layer
@@ -647,6 +647,53 @@ impl AuthApplicationService {
Ok(())
}
/// Update the profile image for a non-OIDC user.
pub async fn update_user_image(
&self,
caller_id: Uuid,
image: Option<String>,
) -> Result<(), DomainError> {
let user = self.user_storage.get_user_by_id(caller_id).await?;
if user.is_oidc_user() {
return Err(DomainError::new(
ErrorKind::AccessDenied,
"User",
"Avatar is managed by your identity provider and cannot be changed here",
));
}
if let Some(ref img) = image {
const MAX_BYTES: usize = 524_288; // 512 KiB
if img.len() > MAX_BYTES {
return Err(DomainError::new(
ErrorKind::InvalidInput,
"User",
"Image exceeds maximum allowed size (512 KiB)",
));
}
let valid = img.starts_with("https://")
|| img.starts_with("http://")
|| img.starts_with("data:image/png;base64,")
|| img.starts_with("data:image/webp;base64,")
|| img.starts_with("data:image/jpeg;base64,");
if !valid {
return Err(DomainError::new(
ErrorKind::InvalidInput,
"User",
"Image must be an https/http URL or a data URI (png, webp, jpeg)",
));
}
}
self.user_storage
.update_image(caller_id, image)
.await
.map_err(DomainError::from)?;
Ok(())
}
pub async fn get_user(&self, user_id: Uuid) -> Result<UserDto, DomainError> {
let user = self.user_storage.get_user_by_id(user_id).await?;
Ok(UserDto::from(user))
@@ -1128,8 +1175,9 @@ impl AuthApplicationService {
.await
{
Ok(mut existing_user) => {
// User exists — update last login
// User exists — update last login and sync avatar from IdP
existing_user.register_login();
existing_user.set_image(claims.picture.clone());
self.user_storage.update_user(existing_user.clone()).await?;
existing_user
}
@@ -1206,7 +1254,7 @@ impl AuthApplicationService {
username = format!("{}_{}", &username[..username.len().min(27)], suffix);
}
let new_user = User::new_oidc(
let mut new_user = User::new_oidc(
username.clone(),
oidc_email,
role,
@@ -1221,6 +1269,7 @@ impl AuthApplicationService {
format!("Failed to create OIDC user: {}", e),
)
})?;
new_user.set_image(claims.picture.clone());
let created_user = self.user_storage.create_user(new_user).await?;