Fix bin/publish: copy docs.dist from project root

Fix bin/publish: use correct .env path for rspade_system
Fix bin/publish script: prevent grep exit code 1 from terminating script

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-10-21 02:08:33 +00:00
commit f6fac6c4bc
79758 changed files with 10547827 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,451 @@
<?php
namespace App\RSpade\Core\Database\Models;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use RuntimeException;
use App\RSpade\Core\Database\Models\Rsx_Model_Abstract;
use App\RSpade\Core\Locks\RsxLocks;
use App\RSpade\Core\Session\Session;
/**
* Abstract base model for site-scoped models with automatic concurrency control
*
* Models extending this class:
* - Automatically scope queries by site_id from session
* - Include site_id column in the database
* - Support soft deletes when configured
* - Provide automatic site-level database locking for write operations
* - Strict enforcement of site boundaries - no cross-site data access
*
* SITE ISOLATION:
* - All queries automatically filtered by current session site_id
* - All saves automatically set site_id from session
* - Changing site_id on existing records is FATAL
* - Site ID 0 used when no site in session (global/unscoped data)
* - No caching of site_id - always reads fresh from session
*
* CONCURRENCY CONTROL:
* Site locks are acquired when Session::get_site_id() is called:
* - Each site gets a READ lock when its site_id is accessed
* - First save() operation upgrades to WRITE lock automatically
* - No automatic transactions - handle manually when needed
*
* When config('rsx.locking.site_always_write') is true:
* - ALL requests start with WRITE lock (no read locks ever)
* - Completely serializes all requests for a given site
* - Severe performance impact - use only for critical operations
* - Displays warning on stderr in production CLI mode
*
* This prevents race conditions for critical operations like:
* - Inventory management
* - Auction bidding
* - Financial transactions
* - Any operation requiring strict consistency within a site
*/
abstract class Rsx_Site_Model_Abstract extends Rsx_Model_Abstract
{
/**
* Whether to automatically apply site scoping to queries
* Can be disabled for admin operations that need cross-site access
*
* @var bool
*/
protected static $apply_site_scope = true;
/**
* Site lock tokens by site_id
* @var array<int, string>
*/
protected static $site_lock_tokens = [];
/**
* Whether each site lock has been upgraded to write
* @var array<int, bool>
*/
protected static $site_lock_is_write = [];
/**
* Get the current site ID from session
* Always returns fresh value - never cached
*
* @return int
*/
public static function get_current_site_id(): int
{
$site_id = Session::get_site_id();
// Use site_id 0 if null/empty (global scope)
if ($site_id === null || $site_id === '') {
return 0;
}
return (int)$site_id;
}
/**
* Acquire a read lock for a specific site ID
* Called from Session::get_site_id() when site_id is accessed
*
* @param int $site_id The site ID to lock (defaults to 0)
* @return void
*/
public static function acquire_site_lock_for_id(int $site_id): void
{
// Don't lock if we already have a lock for this site
if (isset(static::$site_lock_tokens[$site_id])) {
return;
}
$always_write = config('rsx.locking.site_always_write', false);
// Issue warning if always_write_lock is enabled in production CLI mode
if ($always_write && php_sapi_name() === 'cli' && app()->environment('production')) {
fwrite(STDERR, "\033[33mWARNING: RSX_SITE_ALWAYS_WRITE is enabled in production. " .
'All site requests are serialized with exclusive write locks. ' .
"This severely impacts performance and should only be used for critical operations.\033[0m\n");
}
// Determine lock type based on configuration
$lock_type = $always_write ? RsxLocks::WRITE_LOCK : RsxLocks::READ_LOCK;
// Acquire lock for this site
static::$site_lock_tokens[$site_id] = RsxLocks::get_lock(
RsxLocks::DATABASE_LOCK,
RsxLocks::LOCK_SITE_PREFIX . $site_id,
$lock_type,
config('rsx.locking.timeout', 30)
);
// If we started with a write lock, mark it as such
static::$site_lock_is_write[$site_id] = $always_write;
// Register shutdown handler to cleanup (only once)
static $shutdown_registered = false;
if (!$shutdown_registered) {
register_shutdown_function([static::class, 'release_all_site_locks']);
$shutdown_registered = true;
}
}
/**
* Upgrade site lock from read to write
* Called automatically on first save() operation
*
* @return void
*/
protected static function __upgrade_to_write_lock(): void
{
$site_id = static::get_current_site_id();
// If no lock for this site yet, acquire write lock directly
if (!isset(static::$site_lock_tokens[$site_id])) {
static::$site_lock_tokens[$site_id] = RsxLocks::get_lock(
RsxLocks::DATABASE_LOCK,
RsxLocks::LOCK_SITE_PREFIX . $site_id,
RsxLocks::WRITE_LOCK,
config('rsx.locking.timeout', 30)
);
static::$site_lock_is_write[$site_id] = true;
return;
}
// Already have write lock
if (isset(static::$site_lock_is_write[$site_id]) && static::$site_lock_is_write[$site_id]) {
return;
}
// Upgrade read to write
try {
static::$site_lock_tokens[$site_id] = RsxLocks::upgrade_lock(
static::$site_lock_tokens[$site_id],
config('rsx.locking.timeout', 30)
);
static::$site_lock_is_write[$site_id] = true;
} catch (RuntimeException $e) {
throw new RuntimeException(
"Failed to upgrade site lock to write mode for site {$site_id}: " . $e->getMessage()
);
}
}
/**
* Release a specific site's lock
*
* @param int $site_id The site ID to release lock for
* @return void
*/
public static function release_site_lock(int $site_id): void
{
if (isset(static::$site_lock_tokens[$site_id])) {
try {
RsxLocks::release_lock(static::$site_lock_tokens[$site_id]);
} catch (Exception $e) {
// Ignore errors during cleanup
}
unset(static::$site_lock_tokens[$site_id]);
unset(static::$site_lock_is_write[$site_id]);
}
}
/**
* Release all site locks
* Called automatically on shutdown
*
* @return void
*/
public static function release_all_site_locks(): void
{
foreach (static::$site_lock_tokens as $site_id => $token) {
try {
RsxLocks::release_lock($token);
} catch (Exception $e) {
// Ignore errors during cleanup
}
}
static::$site_lock_tokens = [];
static::$site_lock_is_write = [];
}
/**
* Temporarily disable site scoping for admin operations
*
* @param callable $callback
* @return mixed
*/
public static function without_site_scope(callable $callback)
{
$was_applying = static::$apply_site_scope;
static::$apply_site_scope = false;
try {
return $callback();
} finally {
static::$apply_site_scope = $was_applying;
}
}
/**
* Boot the model and add global scope for site_id
*/
protected static function booted()
{
parent::booted();
// Add global scope to filter by site_id
static::addGlobalScope('site', function (Builder $builder) {
if (static::$apply_site_scope) {
$site_id = static::get_current_site_id();
$builder->where($builder->getModel()->getTable() . '.site_id', $site_id);
}
});
// Automatically set site_id when creating new models
static::creating(function ($model) {
if (static::$apply_site_scope) {
// Always set site_id from session, even if already set
// This ensures consistency and prevents injection attacks
$model->site_id = static::get_current_site_id();
}
});
// Validate site_id on save (both create and update)
static::saving(function ($model) {
if (static::$apply_site_scope) {
$current_site_id = static::get_current_site_id();
// For existing records, ensure site_id hasn't changed
if ($model->exists) {
$original_site_id = $model->getOriginal('site_id');
// Fatal error if trying to change site_id
if ($model->site_id != $original_site_id) {
shouldnt_happen(
"Attempted to change site_id from {$original_site_id} to {$model->site_id} " .
'on ' . get_class($model) . " ID {$model->id}. " .
'Changing site_id is not allowed.'
);
}
// Fatal error if record doesn't belong to current site
if ($model->site_id != $current_site_id) {
shouldnt_happen(
'Attempted to save ' . get_class($model) . " ID {$model->id} " .
"with site_id {$model->site_id} but current session site_id is {$current_site_id}. " .
'Cross-site saves are not allowed.'
);
}
} else {
// For new records, force the site_id
$model->site_id = $current_site_id;
}
}
});
// After retrieving records, validate they belong to current site
static::retrieved(function ($model) {
if (static::$apply_site_scope) {
$current_site_id = static::get_current_site_id();
// This shouldn't happen if global scope is working, but double-check
if ($model->site_id != $current_site_id) {
shouldnt_happen(
'Retrieved ' . get_class($model) . " ID {$model->id} " .
"with site_id {$model->site_id} but current session site_id is {$current_site_id}. " .
'Global scope should have prevented this.'
);
}
}
});
}
/**
* Override save to handle site locking
*
* @param array $options
* @return bool
*/
public function save(array $options = [])
{
// Always upgrade to write lock for saves
static::__upgrade_to_write_lock();
// Additional validation before save
if (static::$apply_site_scope) {
$current_site_id = static::get_current_site_id();
// Ensure we're not trying to save a record from wrong site
if ($this->exists && $this->site_id != $current_site_id) {
shouldnt_happen(
'Attempted to save ' . get_class($this) . " with site_id {$this->site_id} " .
"but current session site_id is {$current_site_id}. " .
'This indicates a serious security issue.'
);
}
// Force site_id for new records
if (!$this->exists) {
$this->site_id = $current_site_id;
}
}
return parent::save($options);
}
/**
* Override update to handle site locking
*
* @param array $attributes
* @param array $options
* @return bool
*/
public function update(array $attributes = [], array $options = [])
{
// Fatal if trying to change site_id via update
if (isset($attributes['site_id']) && static::$apply_site_scope) {
if ($attributes['site_id'] != $this->site_id) {
shouldnt_happen(
"Attempted to change site_id via update() from {$this->site_id} to {$attributes['site_id']} " .
'on ' . get_class($this) . " ID {$this->id}. " .
'Changing site_id is never allowed.'
);
}
// Remove site_id from attributes since it shouldn't change
unset($attributes['site_id']);
}
// Always upgrade to write lock for updates
static::__upgrade_to_write_lock();
return parent::update($attributes, $options);
}
/**
* Override delete to handle site locking
*
* @return bool|null
*/
public function delete()
{
// Always upgrade to write lock for updates
static::__upgrade_to_write_lock();
// Validate site ownership before delete
if (static::$apply_site_scope) {
$current_site_id = static::get_current_site_id();
if ($this->site_id != $current_site_id) {
shouldnt_happen(
'Attempted to delete ' . get_class($this) . " ID {$this->id} " .
"with site_id {$this->site_id} but current session site_id is {$current_site_id}. " .
'Cross-site deletes are not allowed.'
);
}
}
return parent::delete();
}
/**
* Scope a query to a specific site
*
* @param Builder $query
* @param int $site_id
* @return Builder
*/
public function scopeForSite($query, $site_id)
{
return $query->where('site_id', $site_id);
}
/**
* Get models for all sites (admin use only)
*
* @return Builder
*/
public static function for_all_sites()
{
return static::without_site_scope(function () {
return static::query();
});
}
/**
* Create a new model instance for a specific site
*
* @param array $attributes
* @param int|null $site_id Override site_id (admin use only)
* @return static
*/
public static function create_for_site(array $attributes = [], ?int $site_id = null)
{
if ($site_id !== null && !static::$apply_site_scope) {
// Admin mode - allow specific site_id
$attributes['site_id'] = $site_id;
} else {
// Normal mode - use session site_id
$attributes['site_id'] = static::get_current_site_id();
}
return static::create($attributes);
}
/**
* Find or create a model for the current site
*
* @param array $attributes
* @param array $values
* @return static
*/
public static function first_or_create_for_site(array $attributes, array $values = [])
{
$site_id = static::get_current_site_id();
$attributes['site_id'] = $site_id;
$values['site_id'] = $site_id;
return static::firstOrCreate($attributes, $values);
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace App\RSpade\Core\Database\Models;
use App\RSpade\Core\Database\Models\Rsx_Model_Abstract;
/**
* Abstract base model for system/internal models
*
* Models extending this class:
* - Represent server-side only metadata
* - Are never exported to JavaScript ORM
* - Include system data like logs, audit trails, IP addresses, etc.
*
* Examples: ip_addresses, activity_logs, system_settings
*/
abstract class Rsx_System_Model_Abstract extends Rsx_Model_Abstract
{
/**
* Mark this as a system model that should never be exported
*
* @var bool
*/
protected $is_system_model = true;
/**
* System models should never be included in JavaScript ORM exports
*
* @return bool
*/
public function is_exportable_to_javascript()
{
return false;
}
/**
* Get metadata indicating this is a system model
*
* @return array
*/
public function get_system_metadata()
{
return [
'is_system' => true,
'exportable' => false,
'model_type' => 'system',
'description' => 'Internal system model - not exported to client'
];
}
/**
* Override to ensure all columns are marked as never export for system models
*
* @return array
*/
public function get_never_exported_columns()
{
// For system models, ALL columns should never be exported
return array_keys($this->getAttributes());
}
}