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
@@ -42,6 +42,28 @@ impl UserPgRepository {
_ => UserRepositoryError::DatabaseError(format!("Database error: {}", err)),
}
}
/// Updates a user's profile image (URL or data URI). Not part of the
/// `UserRepository` trait — called directly from `AuthApplicationService`.
pub async fn update_image(
&self,
user_id: Uuid,
image: Option<String>,
) -> UserRepositoryResult<()> {
sqlx::query(
r#"
UPDATE auth.users
SET image = $2, updated_at = NOW()
WHERE id = $1
"#,
)
.bind(user_id)
.bind(&image)
.execute(&*self.pool)
.await
.map_err(Self::map_sqlx_error)?;
Ok(())
}
}
impl UserRepository for UserPgRepository {
@@ -105,11 +127,11 @@ impl UserRepository for UserPgRepository {
async fn get_user_by_id(&self, id: Uuid) -> UserRepositoryResult<User> {
let row = sqlx::query(
r#"
SELECT
id, username, email, password_hash, role::text as role_text,
storage_quota_bytes, storage_used_bytes,
SELECT
id, username, email, password_hash, role::text as role_text,
storage_quota_bytes, storage_used_bytes,
created_at, updated_at, last_login_at, active,
oidc_provider, oidc_subject
oidc_provider, oidc_subject, image
FROM auth.users
WHERE id = $1
"#,
@@ -140,6 +162,7 @@ impl UserRepository for UserPgRepository {
row.get("active"),
row.get("oidc_provider"),
row.get("oidc_subject"),
row.get("image"),
))
}
@@ -147,11 +170,11 @@ impl UserRepository for UserPgRepository {
async fn get_user_by_username(&self, username: &str) -> UserRepositoryResult<User> {
let row = sqlx::query(
r#"
SELECT
id, username, email, password_hash, role::text as role_text,
storage_quota_bytes, storage_used_bytes,
SELECT
id, username, email, password_hash, role::text as role_text,
storage_quota_bytes, storage_used_bytes,
created_at, updated_at, last_login_at, active,
oidc_provider, oidc_subject
oidc_provider, oidc_subject, image
FROM auth.users
WHERE username = $1
"#,
@@ -182,6 +205,7 @@ impl UserRepository for UserPgRepository {
row.get("active"),
row.get("oidc_provider"),
row.get("oidc_subject"),
row.get("image"),
))
}
@@ -189,11 +213,11 @@ impl UserRepository for UserPgRepository {
async fn get_user_by_email(&self, email: &str) -> UserRepositoryResult<User> {
let row = sqlx::query(
r#"
SELECT
id, username, email, password_hash, role::text as role_text,
storage_quota_bytes, storage_used_bytes,
SELECT
id, username, email, password_hash, role::text as role_text,
storage_quota_bytes, storage_used_bytes,
created_at, updated_at, last_login_at, active,
oidc_provider, oidc_subject
oidc_provider, oidc_subject, image
FROM auth.users
WHERE email = $1
"#,
@@ -224,6 +248,7 @@ impl UserRepository for UserPgRepository {
row.get("active"),
row.get("oidc_provider"),
row.get("oidc_subject"),
row.get("image"),
))
}
@@ -238,7 +263,7 @@ impl UserRepository for UserPgRepository {
sqlx::query(
r#"
UPDATE auth.users
SET
SET
username = $2,
email = $3,
password_hash = $4,
@@ -247,7 +272,8 @@ impl UserRepository for UserPgRepository {
storage_used_bytes = $7,
updated_at = $8,
last_login_at = $9,
active = $10
active = $10,
image = $11
WHERE id = $1
"#,
)
@@ -261,6 +287,7 @@ impl UserRepository for UserPgRepository {
.bind(user_clone.updated_at())
.bind(user_clone.last_login_at())
.bind(user_clone.is_active())
.bind(user_clone.image())
.execute(&mut **tx)
.await
.map_err(Self::map_sqlx_error)?;
@@ -323,11 +350,11 @@ impl UserRepository for UserPgRepository {
async fn list_users(&self, limit: i64, offset: i64) -> UserRepositoryResult<Vec<User>> {
let rows = sqlx::query(
r#"
SELECT
id, username, email, password_hash, role::text as role_text,
storage_quota_bytes, storage_used_bytes,
SELECT
id, username, email, password_hash, role::text as role_text,
storage_quota_bytes, storage_used_bytes,
created_at, updated_at, last_login_at, active,
oidc_provider, oidc_subject
oidc_provider, oidc_subject, image
FROM auth.users
ORDER BY created_at DESC
LIMIT $1 OFFSET $2
@@ -363,6 +390,7 @@ impl UserRepository for UserPgRepository {
row.get("active"),
row.get("oidc_provider"),
row.get("oidc_subject"),
row.get("image"),
)
})
.collect();
@@ -378,7 +406,7 @@ impl UserRepository for UserPgRepository {
id, username, email, password_hash, role::text as role_text,
storage_quota_bytes, storage_used_bytes,
created_at, updated_at, last_login_at, active,
oidc_provider, oidc_subject
oidc_provider, oidc_subject, image
FROM auth.users
WHERE username ILIKE $1 OR email ILIKE $1
ORDER BY username
@@ -414,6 +442,7 @@ impl UserRepository for UserPgRepository {
row.get("active"),
row.get("oidc_provider"),
row.get("oidc_subject"),
row.get("image"),
)
})
.collect();
@@ -496,11 +525,11 @@ impl UserRepository for UserPgRepository {
async fn list_users_by_role(&self, role: &str) -> UserRepositoryResult<Vec<User>> {
let rows = sqlx::query(
r#"
SELECT
id, username, email, password_hash, role::text as role_text,
storage_quota_bytes, storage_used_bytes,
SELECT
id, username, email, password_hash, role::text as role_text,
storage_quota_bytes, storage_used_bytes,
created_at, updated_at, last_login_at, active,
oidc_provider, oidc_subject
oidc_provider, oidc_subject, image
FROM auth.users
WHERE role::text = $1
ORDER BY created_at DESC
@@ -535,6 +564,7 @@ impl UserRepository for UserPgRepository {
row.get("active"),
row.get("oidc_provider"),
row.get("oidc_subject"),
row.get("image"),
)
})
.collect();
@@ -566,11 +596,11 @@ impl UserRepository for UserPgRepository {
) -> UserRepositoryResult<User> {
let row = sqlx::query(
r#"
SELECT
id, username, email, password_hash, role::text as role_text,
storage_quota_bytes, storage_used_bytes,
SELECT
id, username, email, password_hash, role::text as role_text,
storage_quota_bytes, storage_used_bytes,
created_at, updated_at, last_login_at, active,
oidc_provider, oidc_subject
oidc_provider, oidc_subject, image
FROM auth.users
WHERE oidc_provider = $1 AND oidc_subject = $2
"#,
@@ -601,6 +631,7 @@ impl UserRepository for UserPgRepository {
row.get("active"),
row.get("oidc_provider"),
row.get("oidc_subject"),
row.get("image"),
))
}
@@ -67,6 +67,7 @@ struct IdTokenClaims {
name: Option<String>,
groups: Option<Vec<String>>,
nonce: Option<String>,
picture: Option<String>,
// Standard JWT fields
#[allow(dead_code)]
iss: Option<String>,
@@ -90,6 +91,7 @@ struct UserInfoResponse {
preferred_username: Option<String>,
name: Option<String>,
groups: Option<Vec<String>>,
picture: Option<String>,
}
// ============================================================================
@@ -460,6 +462,7 @@ impl OidcServicePort for OidcService {
preferred_username: claims.preferred_username,
name: claims.name,
groups: claims.groups.unwrap_or_default(),
picture: claims.picture,
})
}
@@ -511,6 +514,7 @@ impl OidcServicePort for OidcService {
preferred_username: info.preferred_username,
name: info.name,
groups: info.groups.unwrap_or_default(),
picture: info.picture,
})
}