Add flash alert UX improvements, User_Model fetch security, and SCSS-SCOPE-01 BEM guidance
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -243,6 +243,23 @@ class ScssClassScope_CodeQualityRule extends CodeQualityRule_Abstract
|
||||
}
|
||||
|
||||
if ($matched_filename === null) {
|
||||
// Check if this is a BEM child class (contains __) where the parent is a known component
|
||||
if (strpos($wrapper_class, '__') !== false) {
|
||||
[$parent_class, $child_suffix] = explode('__', $wrapper_class, 2);
|
||||
if (isset($components[$parent_class]) || isset($blade_ids[$parent_class])) {
|
||||
// This is a BEM child class - provide specialized guidance
|
||||
$this->add_violation(
|
||||
$file,
|
||||
1,
|
||||
"BEM child class '.{$wrapper_class}' must be nested within parent component block",
|
||||
".{$wrapper_class} { ... }",
|
||||
$this->build_bem_child_suggestion($parent_class, $child_suffix),
|
||||
'critical'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$this->add_violation(
|
||||
$file,
|
||||
1,
|
||||
@@ -345,6 +362,35 @@ class ScssClassScope_CodeQualityRule extends CodeQualityRule_Abstract
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build suggestion for BEM child class at top level
|
||||
*/
|
||||
private function build_bem_child_suggestion(string $parent_class, string $child_suffix): string
|
||||
{
|
||||
$lines = [];
|
||||
$lines[] = "BEM child classes must be nested within their parent component block,";
|
||||
$lines[] = "not declared at the top level of the SCSS file.";
|
||||
$lines[] = "";
|
||||
$lines[] = "INSTEAD OF:";
|
||||
$lines[] = " .{$parent_class}__{$child_suffix} {";
|
||||
$lines[] = " // styles";
|
||||
$lines[] = " }";
|
||||
$lines[] = "";
|
||||
$lines[] = "USE:";
|
||||
$lines[] = " .{$parent_class} {";
|
||||
$lines[] = " &__{$child_suffix} {";
|
||||
$lines[] = " // styles";
|
||||
$lines[] = " }";
|
||||
$lines[] = " }";
|
||||
$lines[] = "";
|
||||
$lines[] = "This compiles to the same CSS (.{$parent_class}__{$child_suffix})";
|
||||
$lines[] = "but maintains proper component scoping in the source files.";
|
||||
$lines[] = "";
|
||||
$lines[] = "See: php artisan rsx:man scss";
|
||||
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build suggestion for filename mismatch
|
||||
*/
|
||||
|
||||
@@ -466,4 +466,37 @@ class User_Model extends Rsx_Site_Model_Abstract
|
||||
// Default: invitation is pending
|
||||
return self::INVITATION_PENDING;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// AJAX FETCH
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Ajax model fetch - allows JavaScript to load user records
|
||||
* Filters out invite_* fields for security
|
||||
*/
|
||||
#[Ajax_Endpoint_Model_Fetch]
|
||||
public static function fetch($id)
|
||||
{
|
||||
$user = static::withTrashed()->find($id);
|
||||
|
||||
if (!$user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = $user->toArray();
|
||||
|
||||
// Filter out invite_* fields - these contain sensitive invitation data
|
||||
foreach (array_keys($data) as $key) {
|
||||
if (str_starts_with($key, 'invite_')) {
|
||||
unset($data[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
// Augment with computed properties
|
||||
$data['get_full_name'] = $user->get_full_name();
|
||||
$data['get_display_name'] = $user->get_display_name();
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,8 @@
|
||||
* Features:
|
||||
* - Queue system prevents alert spam (2.5s minimum between alerts)
|
||||
* - Auto-dismiss after timeout (success: 4s, others: 6s)
|
||||
* - Hover to pause: Hovering an alert pauses the fadeout timer, resumes on mouse leave
|
||||
* - Text selection: Alert text can be selected and copied
|
||||
* - Smooth fade in/out animations
|
||||
* - Bootstrap alert styling with icons
|
||||
* - Persistent queue across page navigations (sessionStorage, tab-specific)
|
||||
@@ -564,14 +566,57 @@ class Flash_Alert {
|
||||
this._set_fadeout_start_time(message, level, new_fadeout_start_time);
|
||||
}
|
||||
|
||||
// Schedule fadeout
|
||||
if (time_until_fadeout >= 0) {
|
||||
setTimeout(() => {
|
||||
// Track fadeout state on the wrapper element for hover pause/resume
|
||||
$alert_container.data('fadeout_remaining', time_until_fadeout);
|
||||
$alert_container.data('fadeout_paused', false);
|
||||
|
||||
// Schedule fadeout function (reusable for resume)
|
||||
const schedule_fadeout = (delay) => {
|
||||
const timeout_id = setTimeout(() => {
|
||||
// Don't fade if paused (safety check)
|
||||
if ($alert_container.data('fadeout_paused')) return;
|
||||
|
||||
console.log('[Flash_Alert] Fadeout starting for:', message);
|
||||
// Remove from persistence queue when fadeout starts
|
||||
this._remove_from_persistence_queue(message, level);
|
||||
close_alert(1000);
|
||||
}, time_until_fadeout);
|
||||
}, delay);
|
||||
$alert_container.data('fadeout_timeout_id', timeout_id);
|
||||
$alert_container.data('fadeout_scheduled_at', Date.now());
|
||||
};
|
||||
|
||||
// Pause fadeout on hover
|
||||
$alert_container.on('mouseenter', () => {
|
||||
if ($alert_container.data('fadeout_paused')) return;
|
||||
|
||||
const timeout_id = $alert_container.data('fadeout_timeout_id');
|
||||
const scheduled_at = $alert_container.data('fadeout_scheduled_at');
|
||||
const remaining = $alert_container.data('fadeout_remaining');
|
||||
|
||||
if (timeout_id) {
|
||||
clearTimeout(timeout_id);
|
||||
// Calculate how much time was remaining when paused
|
||||
const elapsed = Date.now() - scheduled_at;
|
||||
const new_remaining = Math.max(0, remaining - elapsed);
|
||||
$alert_container.data('fadeout_remaining', new_remaining);
|
||||
$alert_container.data('fadeout_paused', true);
|
||||
console.log('[Flash_Alert] Paused fadeout on hover:', { message, remaining: new_remaining });
|
||||
}
|
||||
});
|
||||
|
||||
// Resume fadeout on mouse leave
|
||||
$alert_container.on('mouseleave', () => {
|
||||
if (!$alert_container.data('fadeout_paused')) return;
|
||||
|
||||
const remaining = $alert_container.data('fadeout_remaining');
|
||||
$alert_container.data('fadeout_paused', false);
|
||||
console.log('[Flash_Alert] Resuming fadeout:', { message, remaining });
|
||||
schedule_fadeout(remaining);
|
||||
});
|
||||
|
||||
// Schedule initial fadeout
|
||||
if (time_until_fadeout >= 0) {
|
||||
schedule_fadeout(time_until_fadeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
.alert {
|
||||
margin-bottom: 0;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
|
||||
// Icon spacing
|
||||
i {
|
||||
|
||||
100
app/RSpade/upstream_changes/user_model_invite_filter_12_26.txt
Executable file
100
app/RSpade/upstream_changes/user_model_invite_filter_12_26.txt
Executable file
@@ -0,0 +1,100 @@
|
||||
USER_MODEL INVITE FIELD FILTERING - MIGRATION GUIDE
|
||||
Date: 2025-12-26
|
||||
|
||||
SUMMARY
|
||||
The framework User_Model now filters out all invite_* fields in its fetch()
|
||||
method for security. Invitation data (invite_code, invite_accepted_at,
|
||||
invite_expires_at) should never be exposed to the client via Ajax fetch.
|
||||
|
||||
If your application overrides User_Model or has a custom user model that
|
||||
extends it, you must apply the same filtering to your fetch() method.
|
||||
|
||||
AFFECTED FILES
|
||||
Any custom User_Model implementations or overrides:
|
||||
- /rsx/models/user_model.php (if exists)
|
||||
- Any model that stores invitation data with invite_* prefixed fields
|
||||
|
||||
CHANGES REQUIRED
|
||||
|
||||
1. Add invite_* Field Filtering to Custom User Models
|
||||
|
||||
If you have a custom fetch() method in a User_Model override, add the
|
||||
invite_* field filtering after toArray():
|
||||
|
||||
BEFORE:
|
||||
#[Ajax_Endpoint_Model_Fetch]
|
||||
public static function fetch($id)
|
||||
{
|
||||
$user = static::withTrashed()->find($id);
|
||||
if (!$user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = $user->toArray();
|
||||
|
||||
// ... your customizations
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
AFTER:
|
||||
#[Ajax_Endpoint_Model_Fetch]
|
||||
public static function fetch($id)
|
||||
{
|
||||
$user = static::withTrashed()->find($id);
|
||||
if (!$user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = $user->toArray();
|
||||
|
||||
// Filter out invite_* fields - these contain sensitive invitation data
|
||||
foreach (array_keys($data) as $key) {
|
||||
if (str_starts_with($key, 'invite_')) {
|
||||
unset($data[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
// ... your customizations
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
2. Check Other Models with Invitation Fields
|
||||
|
||||
If you have other models that store invitation data with invite_* prefix,
|
||||
apply the same pattern:
|
||||
|
||||
// Filter out invite_* fields - sensitive invitation data
|
||||
foreach (array_keys($data) as $key) {
|
||||
if (str_starts_with($key, 'invite_')) {
|
||||
unset($data[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
SECURITY RATIONALE
|
||||
|
||||
Invitation fields contain sensitive data that should never reach the client:
|
||||
|
||||
- invite_code: The secret code used to accept an invitation. Exposing this
|
||||
allows unauthorized invitation acceptance.
|
||||
|
||||
- invite_expires_at: While less sensitive, combined with other data could
|
||||
inform timing attacks.
|
||||
|
||||
- invite_accepted_at: User activity metadata that may have privacy
|
||||
implications.
|
||||
|
||||
VERIFICATION
|
||||
|
||||
1. Test that User_Model.fetch() does not return invite_* fields:
|
||||
|
||||
php artisan rsx:ajax User_Model fetch --args='{"id":1}'
|
||||
|
||||
The response should NOT contain invite_code, invite_accepted_at, or
|
||||
invite_expires_at.
|
||||
|
||||
2. If you have a custom override, verify the same for your model.
|
||||
|
||||
REFERENCE
|
||||
Framework User_Model: system/app/RSpade/Core/Models/User_Model.php
|
||||
Reference in New Issue
Block a user