Harden community profile identity rendering
This commit is contained in:
@@ -1015,19 +1015,12 @@ impl AdminService {
|
|||||||
current_workspace_id: Option<&WorkspaceId>,
|
current_workspace_id: Option<&WorkspaceId>,
|
||||||
payload: UpdateProfilePayload,
|
payload: UpdateProfilePayload,
|
||||||
) -> Result<SessionResponse, ApiError> {
|
) -> Result<SessionResponse, ApiError> {
|
||||||
let display_name = payload.display_name.trim();
|
let display_name = validate_profile_display_name(&payload.display_name)?;
|
||||||
let email = payload.email.trim().to_ascii_lowercase();
|
let email = validate_profile_email(&payload.email)?;
|
||||||
|
|
||||||
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 user = self
|
let user = self
|
||||||
.registry
|
.registry
|
||||||
.update_user_profile(user_id, &email, display_name)
|
.update_user_profile(user_id, &email, &display_name)
|
||||||
.await?;
|
.await?;
|
||||||
let memberships = self.registry.list_workspaces_for_user(user_id).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()
|
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> {
|
fn validate_secret_payload(payload: &SecretPayload) -> Result<(), ApiError> {
|
||||||
if payload.name.trim().is_empty() {
|
if payload.name.trim().is_empty() {
|
||||||
return Err(ApiError::validation("secret name must not be 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 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 {
|
fn cacheable_execution_config() -> ExecutionConfig {
|
||||||
ExecutionConfig {
|
ExecutionConfig {
|
||||||
@@ -4339,6 +4369,26 @@ mod tests {
|
|||||||
"response cache is supported only for REST GET operations"
|
"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(
|
fn enrich_operation_summary(
|
||||||
|
|||||||
@@ -114,6 +114,10 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
z-index: 200;
|
z-index: 200;
|
||||||
}
|
}
|
||||||
|
.user-dropdown[hidden],
|
||||||
|
.mobile-nav[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
.user-dropdown-header {
|
.user-dropdown-header {
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
border-bottom: 1px solid var(--border-subtle);
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
|||||||
+6
-1
@@ -31,11 +31,12 @@
|
|||||||
if (response.status === 401 && window.CrankAuth && typeof window.CrankAuth.handleUnauthorized === 'function') {
|
if (response.status === 401 && window.CrankAuth && typeof window.CrankAuth.handleUnauthorized === 'function') {
|
||||||
window.CrankAuth.handleUnauthorized();
|
window.CrankAuth.handleUnauthorized();
|
||||||
}
|
}
|
||||||
|
var looksLikeHtml = /^\s*</.test(text || '');
|
||||||
var message = payload && payload.error
|
var message = payload && payload.error
|
||||||
? payload.error.message
|
? payload.error.message
|
||||||
: payload && payload.message
|
: payload && payload.message
|
||||||
? payload.message
|
? payload.message
|
||||||
: text
|
: text && !looksLikeHtml
|
||||||
? text
|
? text
|
||||||
: ('HTTP ' + response.status);
|
: ('HTTP ' + response.status);
|
||||||
var error = new Error(message);
|
var error = new Error(message);
|
||||||
@@ -44,6 +45,10 @@
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (text && payload === null) {
|
||||||
|
throw new Error('Backend returned a non-JSON response');
|
||||||
|
}
|
||||||
|
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+21
-7
@@ -79,7 +79,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initials(displayName, email) {
|
function initials(displayName, email) {
|
||||||
var source = displayName || email || 'Crank';
|
var source = safeIdentityText(displayName, 80, '') || safeIdentityText(email, 254, '') || 'Crank';
|
||||||
return source
|
return source
|
||||||
.split(/\s+/)
|
.split(/\s+/)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
@@ -88,12 +88,26 @@
|
|||||||
.join('') || 'CR';
|
.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) {
|
function persistUserMirror(session) {
|
||||||
var membership = primaryMembership(session);
|
var membership = primaryMembership(session);
|
||||||
var user = {
|
var user = {
|
||||||
id: session.user.id,
|
id: session.user.id,
|
||||||
name: session.user.display_name,
|
name: safeIdentityText(session.user.display_name, 80, 'Crank'),
|
||||||
email: session.user.email,
|
email: safeIdentityText(session.user.email, 254, ''),
|
||||||
role: membership ? roleLabel(membership.role) : tKey('workspace_setup.role.viewer'),
|
role: membership ? roleLabel(membership.role) : tKey('workspace_setup.role.viewer'),
|
||||||
workspace: membership ? membership.workspace.slug : '',
|
workspace: membership ? membership.workspace.slug : '',
|
||||||
workspaceId: membership ? membership.workspace.id : '',
|
workspaceId: membership ? membership.workspace.id : '',
|
||||||
@@ -117,11 +131,11 @@
|
|||||||
var membership = primaryMembership(session);
|
var membership = primaryMembership(session);
|
||||||
var fallbackUser = mirroredUser();
|
var fallbackUser = mirroredUser();
|
||||||
var displayName = session && session.user
|
var displayName = session && session.user
|
||||||
? (session.user.display_name || session.user.email || 'Crank')
|
? (safeIdentityText(session.user.display_name, 80, '') || safeIdentityText(session.user.email, 254, '') || 'Crank')
|
||||||
: (fallbackUser && fallbackUser.name) || 'Crank';
|
: safeIdentityText(fallbackUser && fallbackUser.name, 80, 'Crank');
|
||||||
var email = session && session.user
|
var email = session && session.user
|
||||||
? session.user.email
|
? safeIdentityText(session.user.email, 254, '')
|
||||||
: fallbackUser && fallbackUser.email;
|
: safeIdentityText(fallbackUser && fallbackUser.email, 254, '');
|
||||||
var role = membership
|
var role = membership
|
||||||
? roleLabel(membership.role)
|
? roleLabel(membership.role)
|
||||||
: (fallbackUser && fallbackUser.role) || tKey('workspace_setup.role.viewer');
|
: (fallbackUser && fallbackUser.role) || tKey('workspace_setup.role.viewer');
|
||||||
|
|||||||
+22
-6
@@ -13,7 +13,7 @@ function settingsWorkspaceId() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initials(displayName, email) {
|
function initials(displayName, email) {
|
||||||
var source = displayName || email || 'Crank';
|
var source = safeIdentityText(displayName, 80, '') || safeIdentityText(email, 254, '') || 'Crank';
|
||||||
return source
|
return source
|
||||||
.split(/\s+/)
|
.split(/\s+/)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
@@ -22,6 +22,20 @@ function initials(displayName, email) {
|
|||||||
.join('') || 'CR';
|
.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) {
|
function setStatus(id, text, isError) {
|
||||||
var node = document.getElementById(id);
|
var node = document.getElementById(id);
|
||||||
if (!node) return;
|
if (!node) return;
|
||||||
@@ -118,17 +132,19 @@ function populateProfile(session) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var user = session.user;
|
var user = session.user;
|
||||||
document.getElementById('profile-avatar').textContent = initials(user.display_name, user.email);
|
var displayName = safeIdentityText(user.display_name, 80, 'Crank');
|
||||||
document.getElementById('profile-display-name').textContent = user.display_name || 'Crank';
|
var userEmail = safeIdentityText(user.email, 254, '');
|
||||||
document.getElementById('profile-email').textContent = user.email || '';
|
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 firstName = document.getElementById('field-firstname');
|
||||||
var email = document.getElementById('field-email');
|
var email = document.getElementById('field-email');
|
||||||
if (firstName) {
|
if (firstName) {
|
||||||
firstName.value = user.display_name || '';
|
firstName.value = displayName === 'Crank' ? '' : displayName;
|
||||||
}
|
}
|
||||||
if (email) {
|
if (email) {
|
||||||
email.value = user.email || '';
|
email.value = userEmail;
|
||||||
}
|
}
|
||||||
|
|
||||||
populateCurrentSession(session);
|
populateCurrentSession(session);
|
||||||
|
|||||||
Reference in New Issue
Block a user