Harden community profile identity rendering
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -114,6 +114,10 @@
|
||||
overflow: hidden;
|
||||
z-index: 200;
|
||||
}
|
||||
.user-dropdown[hidden],
|
||||
.mobile-nav[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
.user-dropdown-header {
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
|
||||
+6
-1
@@ -31,11 +31,12 @@
|
||||
if (response.status === 401 && window.CrankAuth && typeof window.CrankAuth.handleUnauthorized === 'function') {
|
||||
window.CrankAuth.handleUnauthorized();
|
||||
}
|
||||
var looksLikeHtml = /^\s*</.test(text || '');
|
||||
var message = payload && payload.error
|
||||
? payload.error.message
|
||||
: payload && payload.message
|
||||
? payload.message
|
||||
: text
|
||||
: text && !looksLikeHtml
|
||||
? text
|
||||
: ('HTTP ' + response.status);
|
||||
var error = new Error(message);
|
||||
@@ -44,6 +45,10 @@
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (text && payload === null) {
|
||||
throw new Error('Backend returned a non-JSON response');
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
|
||||
+21
-7
@@ -79,7 +79,7 @@
|
||||
}
|
||||
|
||||
function initials(displayName, email) {
|
||||
var source = displayName || email || 'Crank';
|
||||
var source = safeIdentityText(displayName, 80, '') || safeIdentityText(email, 254, '') || 'Crank';
|
||||
return source
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
@@ -88,12 +88,26 @@
|
||||
.join('') || 'CR';
|
||||
}
|
||||
|
||||
function safeIdentityText(value, maxLength, fallback) {
|
||||
var text = String(value || '')
|
||||
.replace(/[\r\n\t]+/g, ' ')
|
||||
.replace(/\s{2,}/g, ' ')
|
||||
.trim();
|
||||
if (!text || /<\s*(?:!doctype|html|head|body|nav|script)\b/i.test(text)) {
|
||||
return fallback || '';
|
||||
}
|
||||
if (text.length > maxLength) {
|
||||
return fallback || '';
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function persistUserMirror(session) {
|
||||
var membership = primaryMembership(session);
|
||||
var user = {
|
||||
id: session.user.id,
|
||||
name: session.user.display_name,
|
||||
email: session.user.email,
|
||||
name: safeIdentityText(session.user.display_name, 80, 'Crank'),
|
||||
email: safeIdentityText(session.user.email, 254, ''),
|
||||
role: membership ? roleLabel(membership.role) : tKey('workspace_setup.role.viewer'),
|
||||
workspace: membership ? membership.workspace.slug : '',
|
||||
workspaceId: membership ? membership.workspace.id : '',
|
||||
@@ -117,11 +131,11 @@
|
||||
var membership = primaryMembership(session);
|
||||
var fallbackUser = mirroredUser();
|
||||
var displayName = session && session.user
|
||||
? (session.user.display_name || session.user.email || 'Crank')
|
||||
: (fallbackUser && fallbackUser.name) || 'Crank';
|
||||
? (safeIdentityText(session.user.display_name, 80, '') || safeIdentityText(session.user.email, 254, '') || 'Crank')
|
||||
: safeIdentityText(fallbackUser && fallbackUser.name, 80, 'Crank');
|
||||
var email = session && session.user
|
||||
? session.user.email
|
||||
: fallbackUser && fallbackUser.email;
|
||||
? safeIdentityText(session.user.email, 254, '')
|
||||
: safeIdentityText(fallbackUser && fallbackUser.email, 254, '');
|
||||
var role = membership
|
||||
? roleLabel(membership.role)
|
||||
: (fallbackUser && fallbackUser.role) || tKey('workspace_setup.role.viewer');
|
||||
|
||||
+22
-6
@@ -13,7 +13,7 @@ function settingsWorkspaceId() {
|
||||
}
|
||||
|
||||
function initials(displayName, email) {
|
||||
var source = displayName || email || 'Crank';
|
||||
var source = safeIdentityText(displayName, 80, '') || safeIdentityText(email, 254, '') || 'Crank';
|
||||
return source
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
@@ -22,6 +22,20 @@ function initials(displayName, email) {
|
||||
.join('') || 'CR';
|
||||
}
|
||||
|
||||
function safeIdentityText(value, maxLength, fallback) {
|
||||
var text = String(value || '')
|
||||
.replace(/[\r\n\t]+/g, ' ')
|
||||
.replace(/\s{2,}/g, ' ')
|
||||
.trim();
|
||||
if (!text || /<\s*(?:!doctype|html|head|body|nav|script)\b/i.test(text)) {
|
||||
return fallback || '';
|
||||
}
|
||||
if (text.length > maxLength) {
|
||||
return fallback || '';
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function setStatus(id, text, isError) {
|
||||
var node = document.getElementById(id);
|
||||
if (!node) return;
|
||||
@@ -118,17 +132,19 @@ function populateProfile(session) {
|
||||
}
|
||||
|
||||
var user = session.user;
|
||||
document.getElementById('profile-avatar').textContent = initials(user.display_name, user.email);
|
||||
document.getElementById('profile-display-name').textContent = user.display_name || 'Crank';
|
||||
document.getElementById('profile-email').textContent = user.email || '';
|
||||
var displayName = safeIdentityText(user.display_name, 80, 'Crank');
|
||||
var userEmail = safeIdentityText(user.email, 254, '');
|
||||
document.getElementById('profile-avatar').textContent = initials(displayName, userEmail);
|
||||
document.getElementById('profile-display-name').textContent = displayName || 'Crank';
|
||||
document.getElementById('profile-email').textContent = userEmail;
|
||||
|
||||
var firstName = document.getElementById('field-firstname');
|
||||
var email = document.getElementById('field-email');
|
||||
if (firstName) {
|
||||
firstName.value = user.display_name || '';
|
||||
firstName.value = displayName === 'Crank' ? '' : displayName;
|
||||
}
|
||||
if (email) {
|
||||
email.value = user.email || '';
|
||||
email.value = userEmail;
|
||||
}
|
||||
|
||||
populateCurrentSession(session);
|
||||
|
||||
Reference in New Issue
Block a user