Harden community profile identity rendering
Deploy / deploy (push) Successful in 2m9s
CI / Rust Checks (push) Successful in 5m59s
CI / UI Checks (push) Successful in 6s
CI / Deployment Manifests (push) Successful in 3s
CI / Frontend E2E (push) Successful in 4m24s

This commit is contained in:
github-ops
2026-06-19 19:06:48 +00:00
parent db97d37868
commit cbfa412ce0
5 changed files with 114 additions and 25 deletions
+61 -11
View File
@@ -1015,19 +1015,12 @@ impl AdminService {
current_workspace_id: Option<&WorkspaceId>,
payload: UpdateProfilePayload,
) -> Result<SessionResponse, ApiError> {
let display_name = payload.display_name.trim();
let email = payload.email.trim().to_ascii_lowercase();
if display_name.is_empty() {
return Err(ApiError::validation("display name is required"));
}
if email.is_empty() || !email.contains('@') {
return Err(ApiError::validation("a valid email address is required"));
}
let display_name = validate_profile_display_name(&payload.display_name)?;
let email = validate_profile_email(&payload.email)?;
let user = self
.registry
.update_user_profile(user_id, &email, display_name)
.update_user_profile(user_id, &email, &display_name)
.await?;
let memberships = self.registry.list_workspaces_for_user(user_id).await?;
@@ -3717,6 +3710,41 @@ fn normalize_base_url(value: &str) -> String {
value.trim().trim_end_matches('/').to_owned()
}
fn validate_profile_display_name(value: &str) -> Result<String, ApiError> {
let display_name = value.trim();
if display_name.is_empty() {
return Err(ApiError::validation("display name is required"));
}
if display_name.chars().count() > 80 {
return Err(ApiError::validation(
"display name must be at most 80 characters",
));
}
if display_name
.chars()
.any(|character| character.is_control() || matches!(character, '<' | '>'))
{
return Err(ApiError::validation(
"display name contains unsupported characters",
));
}
Ok(display_name.to_owned())
}
fn validate_profile_email(value: &str) -> Result<String, ApiError> {
let email = value.trim().to_ascii_lowercase();
if email.is_empty()
|| email.len() > 254
|| !email.contains('@')
|| email.chars().any(|character| {
character.is_whitespace() || character.is_control() || matches!(character, '<' | '>')
})
{
return Err(ApiError::validation("a valid email address is required"));
}
Ok(email)
}
fn validate_secret_payload(payload: &SecretPayload) -> Result<(), ApiError> {
if payload.name.trim().is_empty() {
return Err(ApiError::validation("secret name must not be empty"));
@@ -4291,7 +4319,9 @@ mod tests {
use crank_core::{ExecutionConfig, HttpMethod, ResponseCachePolicy, RestTarget, Target};
use super::validate_response_cache_policy;
use super::{
validate_profile_display_name, validate_profile_email, validate_response_cache_policy,
};
fn cacheable_execution_config() -> ExecutionConfig {
ExecutionConfig {
@@ -4339,6 +4369,26 @@ mod tests {
"response cache is supported only for REST GET operations"
);
}
#[test]
fn validates_profile_identity_fields() {
assert_eq!(
validate_profile_display_name(" Updated Owner ").unwrap(),
"Updated Owner"
);
assert_eq!(
validate_profile_email(" OWNER@CRANK.LOCAL ").unwrap(),
"owner@crank.local"
);
}
#[test]
fn rejects_profile_identity_page_text() {
assert!(validate_profile_display_name("<html>Crank</html>").is_err());
assert!(validate_profile_display_name("Crank\nOperations\nSave profile").is_err());
assert!(validate_profile_display_name(&"x".repeat(81)).is_err());
assert!(validate_profile_email("owner <html>@crank.local").is_err());
}
}
fn enrich_operation_summary(