feat(user.prefered_locale): save user's locale + invited have same locale as inviters

- OIDC JIT define the locale only at user creation, user can so change his preference later
    - invited users will inherit inviter's locale
    - email will use prefered_locale
    - login to a new browser will use prefered_locale
This commit is contained in:
Edouard Vanbelle
2026-06-03 14:26:45 +02:00
parent 854f1d3a07
commit 7db27af7a6
14 changed files with 342 additions and 26 deletions
@@ -96,10 +96,11 @@ impl UserRepository for UserPgRepository {
storage_quota_bytes, storage_used_bytes,
created_at, updated_at, last_login_at, active,
oidc_provider, oidc_subject, is_external,
given_name, family_name, email_verified_at
given_name, family_name, email_verified_at,
preferred_locale
) VALUES (
$1, $2, $3, $4, $5::auth.userrole, $6, $7, $8, $9, $10, $11,
$12, $13, $14, $15, $16, $17
$12, $13, $14, $15, $16, $17, $18
)
RETURNING *
"#,
@@ -121,6 +122,7 @@ impl UserRepository for UserPgRepository {
.bind(user_clone.given_name())
.bind(user_clone.family_name())
.bind(user_clone.email_verified_at())
.bind(user_clone.preferred_locale())
.execute(&mut **tx)
.await
.map_err(Self::map_sqlx_error)?;
@@ -145,7 +147,7 @@ impl UserRepository for UserPgRepository {
storage_quota_bytes, storage_used_bytes,
created_at, updated_at, last_login_at, active,
oidc_provider, oidc_subject, image, is_external,
given_name, family_name, email_verified_at
given_name, family_name, email_verified_at, preferred_locale
FROM auth.users
WHERE id = $1
"#,
@@ -181,6 +183,7 @@ impl UserRepository for UserPgRepository {
row.get("given_name"),
row.get("family_name"),
row.get("email_verified_at"),
row.get("preferred_locale"),
))
}
@@ -193,7 +196,7 @@ impl UserRepository for UserPgRepository {
storage_quota_bytes, storage_used_bytes,
created_at, updated_at, last_login_at, active,
oidc_provider, oidc_subject, image, is_external,
given_name, family_name, email_verified_at
given_name, family_name, email_verified_at, preferred_locale
FROM auth.users
WHERE username = $1
"#,
@@ -229,6 +232,7 @@ impl UserRepository for UserPgRepository {
row.get("given_name"),
row.get("family_name"),
row.get("email_verified_at"),
row.get("preferred_locale"),
))
}
@@ -241,7 +245,7 @@ impl UserRepository for UserPgRepository {
storage_quota_bytes, storage_used_bytes,
created_at, updated_at, last_login_at, active,
oidc_provider, oidc_subject, image, is_external,
given_name, family_name, email_verified_at
given_name, family_name, email_verified_at, preferred_locale
FROM auth.users
WHERE email = $1
"#,
@@ -277,6 +281,7 @@ impl UserRepository for UserPgRepository {
row.get("given_name"),
row.get("family_name"),
row.get("email_verified_at"),
row.get("preferred_locale"),
))
}
@@ -304,7 +309,8 @@ impl UserRepository for UserPgRepository {
image = $11,
given_name = $12,
family_name = $13,
email_verified_at = $14
email_verified_at = $14,
preferred_locale = $15
WHERE id = $1
"#,
)
@@ -322,6 +328,7 @@ impl UserRepository for UserPgRepository {
.bind(user_clone.given_name())
.bind(user_clone.family_name())
.bind(user_clone.email_verified_at())
.bind(user_clone.preferred_locale())
.execute(&mut **tx)
.await
.map_err(Self::map_sqlx_error)?;
@@ -394,7 +401,7 @@ impl UserRepository for UserPgRepository {
storage_quota_bytes, storage_used_bytes,
created_at, updated_at, last_login_at, active,
oidc_provider, oidc_subject, image, is_external,
given_name, family_name, email_verified_at
given_name, family_name, email_verified_at, preferred_locale
FROM auth.users
WHERE ($3 OR is_external = FALSE)
ORDER BY created_at DESC
@@ -437,6 +444,7 @@ impl UserRepository for UserPgRepository {
row.get("given_name"),
row.get("family_name"),
row.get("email_verified_at"),
row.get("preferred_locale"),
)
})
.collect();
@@ -458,7 +466,7 @@ impl UserRepository for UserPgRepository {
storage_quota_bytes, storage_used_bytes,
created_at, updated_at, last_login_at, active,
oidc_provider, oidc_subject, image, is_external,
given_name, family_name, email_verified_at
given_name, family_name, email_verified_at, preferred_locale
FROM auth.users
WHERE (username ILIKE $1 OR email ILIKE $1)
AND ($3 OR is_external = FALSE)
@@ -501,6 +509,7 @@ impl UserRepository for UserPgRepository {
row.get("given_name"),
row.get("family_name"),
row.get("email_verified_at"),
row.get("preferred_locale"),
)
})
.collect();
@@ -588,7 +597,7 @@ impl UserRepository for UserPgRepository {
storage_quota_bytes, storage_used_bytes,
created_at, updated_at, last_login_at, active,
oidc_provider, oidc_subject, image, is_external,
given_name, family_name, email_verified_at
given_name, family_name, email_verified_at, preferred_locale
FROM auth.users
WHERE role::text = $1
ORDER BY created_at DESC
@@ -628,6 +637,7 @@ impl UserRepository for UserPgRepository {
row.get("given_name"),
row.get("family_name"),
row.get("email_verified_at"),
row.get("preferred_locale"),
)
})
.collect();
@@ -664,7 +674,7 @@ impl UserRepository for UserPgRepository {
storage_quota_bytes, storage_used_bytes,
created_at, updated_at, last_login_at, active,
oidc_provider, oidc_subject, image, is_external,
given_name, family_name, email_verified_at
given_name, family_name, email_verified_at, preferred_locale
FROM auth.users
WHERE oidc_provider = $1 AND oidc_subject = $2
"#,
@@ -700,6 +710,7 @@ impl UserRepository for UserPgRepository {
row.get("given_name"),
row.get("family_name"),
row.get("email_verified_at"),
row.get("preferred_locale"),
))
}
@@ -70,6 +70,7 @@ struct IdTokenClaims {
groups: Option<Vec<String>>,
nonce: Option<String>,
picture: Option<String>,
locale: Option<String>,
// Standard JWT fields
#[allow(dead_code)]
iss: Option<String>,
@@ -96,6 +97,7 @@ struct UserInfoResponse {
family_name: Option<String>,
groups: Option<Vec<String>>,
picture: Option<String>,
locale: Option<String>,
}
// ============================================================================
@@ -469,6 +471,7 @@ impl OidcServicePort for OidcService {
family_name: claims.family_name,
groups: claims.groups.unwrap_or_default(),
picture: claims.picture,
locale: claims.locale,
})
}
@@ -523,6 +526,7 @@ impl OidcServicePort for OidcService {
family_name: info.family_name,
groups: info.groups.unwrap_or_default(),
picture: info.picture,
locale: info.locale,
})
}