Add client-side Permission class and resolved_permissions to rsxapp

Refactor date/time classes to reduce code redundancy

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2026-01-13 08:35:06 +00:00
parent f70ca09f78
commit 8ef798c30f
11 changed files with 783 additions and 466 deletions

View File

@@ -4,17 +4,19 @@ NAME
acls - Role-based access control with supplementary permissions
SYNOPSIS
// Check if current user has a permission
Permission::has_permission(User_Model::PERM_MANAGE_SITE_USERS)
PHP (server-side):
Permission::has_permission(User_Model::PERM_MANAGE_SITE_USERS)
Permission::has_role(User_Model::ROLE_SITE_ADMIN)
$user->has_permission(User_Model::PERM_EDIT_DATA)
$user->get_resolved_permissions() // All permissions as array
$user->can_admin_role($target_user->role_id)
// Check if current user has at least a certain role
Permission::has_role(User_Model::ROLE_SITE_ADMIN)
// Check on specific user instance
$user->has_permission(User_Model::PERM_EDIT_DATA)
// Check if user can administer another user's role
$user->can_admin_role($target_user->role_id)
JavaScript (client-side):
Permission.has_permission(User_Model.PERM_EDIT_DATA)
Permission.has_any_permission([User_Model.PERM_EDIT_DATA, User_Model.PERM_VIEW_DATA])
Permission.has_all_permissions([...])
Permission.has_role(User_Model.ROLE_MANAGER)
Permission.can_admin_role(User_Model.ROLE_USER)
DESCRIPTION
RSpade provides a role-based access control (RBAC) system where:
@@ -72,19 +74,20 @@ ARCHITECTURE
Role Hierarchy
ID Constant Label Can Admin Roles
-- -------- ----- ---------------
1 ROLE_ROOT_ADMIN Root Admin 2,3,4,5,6,7
2 ROLE_SITE_OWNER Site Owner 3,4,5,6,7
3 ROLE_SITE_ADMIN Site Admin 4,5,6,7
4 ROLE_MANAGER Manager 5,6,7
5 ROLE_USER User (none)
6 ROLE_VIEWER Viewer (none)
7 ROLE_DISABLED Disabled (none)
ID Constant Label Can Admin Roles
--- -------- ----- ---------------
100 ROLE_DEVELOPER Developer 200-800 (system only)
200 ROLE_ROOT_ADMIN Root Admin 300-800 (system only)
300 ROLE_SITE_OWNER Site Owner 400-800
400 ROLE_SITE_ADMIN Site Admin 500-800
500 ROLE_MANAGER Manager 600-800
600 ROLE_USER User (none)
700 ROLE_VIEWER Viewer (none)
800 ROLE_DISABLED Disabled (none)
"Can Admin Roles" means a user with that role can create, edit,
or change the role of users with the listed role IDs. This
prevents privilege escalation (admin can't create root admin).
IDs are 100-based for future expansion. Lower ID = higher privilege.
"Can Admin Roles" prevents privilege escalation (Site Admin can't
create Site Owner). Developer and Root Admin are system-assigned only.
PERMISSIONS
@@ -92,7 +95,7 @@ PERMISSIONS
ID Constant Granted By Default To
-- -------- ---------------------
1 PERM_MANAGE_SITES_ROOT Root Admin only
1 PERM_MANAGE_SITES_ROOT Developer, Root Admin only
2 PERM_MANAGE_SITE_BILLING Site Owner+
3 PERM_MANAGE_SITE_SETTINGS Site Admin+
4 PERM_MANAGE_SITE_USERS Site Admin+
@@ -109,17 +112,17 @@ PERMISSIONS
Role-Permission Matrix
Permission Root Owner Admin Mgr User View Dis
---------- ---- ----- ----- --- ---- ---- ---
MANAGE_SITES_ROOT X
MANAGE_SITE_BILLING X X
MANAGE_SITE_SETTINGS X X X
MANAGE_SITE_USERS X X X
VIEW_USER_ACTIVITY X X X X
EDIT_DATA X X X X X
VIEW_DATA X X X X X X
API_ACCESS - - - - - -
DATA_EXPORT - - - - - -
Permission Dev Root Owner Admin Mgr User View Dis
---------- --- ---- ----- ----- --- ---- ---- ---
MANAGE_SITES_ROOT X X
MANAGE_SITE_BILLING X X X
MANAGE_SITE_SETTINGS X X X X
MANAGE_SITE_USERS X X X X
VIEW_USER_ACTIVITY X X X X X
EDIT_DATA X X X X X X
VIEW_DATA X X X X X X X
API_ACCESS - - - - - - -
DATA_EXPORT - - - - - - -
Legend: X = granted by role, - = must be granted individually
@@ -129,14 +132,15 @@ MODEL IMPLEMENTATION
class User_Model extends Rsx_Model_Abstract
{
// Role constants
const ROLE_ROOT_ADMIN = 1;
const ROLE_SITE_OWNER = 2;
const ROLE_SITE_ADMIN = 3;
const ROLE_MANAGER = 4;
const ROLE_USER = 5;
const ROLE_VIEWER = 6;
const ROLE_DISABLED = 7;
// Role constants (100-based, lower = higher privilege)
const ROLE_DEVELOPER = 100;
const ROLE_ROOT_ADMIN = 200;
const ROLE_SITE_OWNER = 300;
const ROLE_SITE_ADMIN = 400;
const ROLE_MANAGER = 500;
const ROLE_USER = 600;
const ROLE_VIEWER = 700;
const ROLE_DISABLED = 800;
// Permission constants
const PERM_MANAGE_SITES_ROOT = 1;
@@ -151,55 +155,44 @@ MODEL IMPLEMENTATION
public static $enums = [
'role_id' => [
self::ROLE_ROOT_ADMIN => [
'constant' => 'ROLE_ROOT_ADMIN',
'label' => 'Root Admin',
'permissions' => [
self::PERM_MANAGE_SITES_ROOT,
self::PERM_MANAGE_SITE_BILLING,
self::PERM_MANAGE_SITE_SETTINGS,
self::PERM_MANAGE_SITE_USERS,
self::PERM_VIEW_USER_ACTIVITY,
self::PERM_EDIT_DATA,
self::PERM_VIEW_DATA,
],
'can_admin_roles' => [2,3,4,5,6,7],
300 => [
'constant' => 'ROLE_SITE_OWNER',
'label' => 'Site Owner',
'permissions' => [2, 3, 4, 5, 6, 7],
'can_admin_roles' => [400, 500, 600, 700, 800],
],
// ... additional roles
],
];
public function has_permission(int $permission): bool
// Get all resolved permissions (role + supplementary applied)
public function get_resolved_permissions(): array
{
if ($this->role_id === self::ROLE_DISABLED) {
return false;
return [];
}
$permissions = $this->role_id__permissions ?? [];
// Add supplementary GRANTs, remove supplementary DENYs
// ... (see User_Model for full implementation)
return $permissions;
}
// Check supplementary DENY (overrides everything)
if ($this->has_supplementary_deny($permission)) {
return false;
}
// Check supplementary GRANT
if ($this->has_supplementary_grant($permission)) {
return true;
}
// Check role default permissions
return in_array($permission, $this->role_permissions ?? []);
public function has_permission(int $permission): bool
{
return in_array($permission, $this->get_resolved_permissions(), true);
}
public function can_admin_role(int $role_id): bool
{
return in_array($role_id, $this->role_can_admin_roles ?? []);
return in_array($role_id, $this->role_id__can_admin_roles ?? [], true);
}
}
Magic Properties (via enum system)
Magic Properties (via enum system, BEM-style double underscore)
$user->role_label // "Site Admin"
$user->role_permissions // [3,4,5,6,7]
$user->role_can_admin_roles // [4,5,6,7]
$user->role_id__label // "Site Admin"
$user->role_id__permissions // [3,4,5,6,7]
$user->role_id__can_admin_roles // [400,500,600,700,800]
PERMISSION CLASS API
@@ -235,6 +228,10 @@ PERMISSION CLASS API
$user->has_permission(int $permission): bool
Check if this specific user has permission.
$user->get_resolved_permissions(): array
Get all resolved permission IDs for this user.
Applies role defaults + grants - denies.
$user->can_admin_role(int $role_id): bool
Check if user can create/edit users with given role.
@@ -244,6 +241,78 @@ PERMISSION CLASS API
$user->has_supplementary_deny(int $permission): bool
Check if user has explicit DENY for permission.
JAVASCRIPT PERMISSION CLASS
The Permission class provides client-side permission checking using
pre-resolved permissions from window.rsxapp.resolved_permissions.
This array is computed server-side and includes role defaults with
supplementary grants added and denies removed, ensuring JS checks
match PHP exactly.
Static Methods
Permission.is_logged_in(): boolean
Check if user is authenticated.
if (Permission.is_logged_in()) {
// Show authenticated UI
}
Permission.get_user(): Object|null
Get current user object from rsxapp.
Permission.has_permission(permission): boolean
Check if user has specific permission.
if (Permission.has_permission(User_Model.PERM_EDIT_DATA)) {
// Show edit button
}
Permission.has_any_permission(permissions): boolean
Check if user has ANY of the listed permissions.
if (Permission.has_any_permission([
User_Model.PERM_EDIT_DATA,
User_Model.PERM_VIEW_DATA
])) {
// User can view or edit
}
Permission.has_all_permissions(permissions): boolean
Check if user has ALL of the listed permissions.
if (Permission.has_all_permissions([
User_Model.PERM_MANAGE_SITE_USERS,
User_Model.PERM_VIEW_USER_ACTIVITY
])) {
// User can manage users AND view activity
}
Permission.has_role(role_id): boolean
Check if user has at least the specified role level.
Lower role_id = higher privilege.
if (Permission.has_role(User_Model.ROLE_MANAGER)) {
// User is Manager or higher (Admin, Owner, etc.)
}
Permission.can_admin_role(role_id): boolean
Check if user can administer users with given role.
if (Permission.can_admin_role(User_Model.ROLE_USER)) {
// Show role assignment dropdown including User role
}
Permission.get_resolved_permissions(): number[]
Get array of all permission IDs the user has.
Data Source
The Permission class reads from window.rsxapp.resolved_permissions,
which is populated by the bundle renderer from the session user's
get_resolved_permissions() result. Empty array if not authenticated.
ROUTE PROTECTION
Using #[Auth] Attribute
@@ -443,7 +512,7 @@ ADDING NEW PERMISSIONS
2. Add to role definitions in $enums if role should grant it:
self::ROLE_SITE_ADMIN => [
400 => [ // ROLE_SITE_ADMIN
'permissions' => [
// ... existing
self::PERM_NEW_FEATURE,
@@ -460,15 +529,13 @@ ADDING NEW PERMISSIONS
ADDING NEW ROLES
1. Add constant (maintain hierarchy order):
1. Add constant (maintain hierarchy order, 100-based):
const ROLE_SUPERVISOR = 4; // Between Admin and Manager
const ROLE_MANAGER = 5; // Renumber if needed
// ...
const ROLE_SUPERVISOR = 450; // Between Admin (400) and Manager (500)
2. Add to $enums with permissions and can_admin_roles:
self::ROLE_SUPERVISOR => [
450 => [
'constant' => 'ROLE_SUPERVISOR',
'label' => 'Supervisor',
'permissions' => [
@@ -476,13 +543,13 @@ ADDING NEW ROLES
self::PERM_EDIT_DATA,
self::PERM_VIEW_DATA,
],
'can_admin_roles' => [5,6,7],
'can_admin_roles' => [500, 600, 700, 800],
],
3. Update can_admin_roles for roles above:
self::ROLE_SITE_ADMIN => [
'can_admin_roles' => [4,5,6,7], // Add new role ID
400 => [ // ROLE_SITE_ADMIN
'can_admin_roles' => [450, 500, 600, 700, 800], // Add new role ID
],
4. Run migration if role_id column needs updating
@@ -594,5 +661,6 @@ SEE ALSO
enums - Enum system for role/permission metadata
routing - Route protection with #[Auth] attribute
session - Session management and user context
rsxapp - Global JS object containing resolved_permissions
RSpade 1.0 November 2024 ACLS(7)
RSpade 1.0 January 2026 ACLS(7)

108
app/RSpade/man/pagedata.txt Executable file
View File

@@ -0,0 +1,108 @@
PAGEDATA(3) RSX Framework Manual PAGEDATA(3)
NAME
PageData - Pass server-side data to JavaScript via window.rsxapp.page_data
SYNOPSIS
PHP Controller:
PageData::add(['key' => $value, 'another' => $data]);
Blade Directive:
@rsx_page_data(['key' => $value])
JavaScript Access:
const value = window.rsxapp.page_data.key;
DESCRIPTION
PageData provides a simple mechanism for passing server-side data to
JavaScript. Data added via PageData::add() or @rsx_page_data is
accumulated during request processing and automatically included in
window.rsxapp.page_data when the bundle renders.
This is useful for:
- Passing IDs needed by JavaScript components
- Pre-loading configuration for client-side logic
- Sharing computed values without additional Ajax calls
USAGE IN BLADE ROUTES
For traditional Blade views, use the @rsx_page_data directive:
{{-- In your Blade view --}}
@rsx_page_data(['user_id' => $user->id, 'can_edit' => $can_edit])
<div id="user-profile">
...
</div>
Or call PageData::add() in the controller before returning the view:
use App\RSpade\Core\View\PageData;
#[Route('/users/:id')]
public static function view(Request $request, array $params = [])
{
$user = User_Model::findOrFail($params['id']);
PageData::add([
'user_id' => $user->id,
'permissions' => $user->get_permissions(),
]);
return rsx_view('User_View', ['user' => $user]);
}
USAGE IN SPA CONTROLLERS
For SPA entry points, call PageData::add() before returning rsx_view(SPA):
use App\RSpade\Core\View\PageData;
#[SPA]
public static function index(Request $request, array $params = [])
{
// Load data needed by SPA actions
$internal_contact = Contact_Model::where('type_id', Contact_Model::TYPE_INTERNAL)
->first();
PageData::add([
'contact_internal_id' => $internal_contact?->id,
]);
return rsx_view(SPA, ['bundle' => 'Frontend_Bundle']);
}
The data is then available in any SPA action or component:
class Sidebar_Component {
on_ready() {
const internal_id = window.rsxapp.page_data.contact_internal_id;
if (internal_id) {
this.$sid('internal_link').attr('href', Rsx.Route('Contact_View_Action', internal_id));
}
}
}
MULTIPLE CALLS
PageData::add() merges data, so you can call it multiple times:
PageData::add(['user_id' => $user->id]);
PageData::add(['site_config' => $config]); // Merged with previous
Later calls overwrite earlier keys with the same name.
API
PageData::add(array $data)
Add key-value pairs to page_data. Merged with existing data.
PageData::get()
Returns all accumulated page data (used internally by bundle renderer).
PageData::has_data()
Returns true if any page data has been set (used internally).
SEE ALSO
rsxapp(3), spa(3), bundle_api(3)
AUTHOR
RSpade Framework
RSpade January 2026 PAGEDATA(3)

207
app/RSpade/man/rsxapp.txt Executable file
View File

@@ -0,0 +1,207 @@
RSXAPP(3) RSX Framework Manual RSXAPP(3)
NAME
rsxapp - Global JavaScript object containing runtime configuration and data
SYNOPSIS
JavaScript:
window.rsxapp.build_key // Manifest build hash
window.rsxapp.user // Current user model data
window.rsxapp.site // Current site model data
window.rsxapp.resolved_permissions // Pre-computed user permissions
window.rsxapp.page_data // Custom page-specific data
window.rsxapp.is_spa // Whether current page is SPA
window.rsxapp.csrf // CSRF token for forms
DESCRIPTION
window.rsxapp is a global JavaScript object rendered with every page bundle.
It contains session data, configuration, and runtime state needed by
client-side JavaScript. The object is built during bundle rendering in
Rsx_Bundle_Abstract::__generate_html() and output as an inline <script> tag
before bundle assets load.
The rsxapp object provides:
- Session context (user, site, authentication state)
- Build information for cache management
- Server time for client-server synchronization
- Custom page data passed via PageData::add()
- Debug configuration in development mode
HOW IT WORKS
When a bundle renders (e.g., Frontend_Bundle::render()), the framework:
1. Collects runtime data from various sources
2. Merges custom page_data from PageData::add() calls
3. JSON-encodes the data
4. Outputs: <script>window.rsxapp = {...};</script>
5. Bundle script tags follow, using rsxapp for initialization
Framework core classes (Rsx_Time, Rsx_Storage, Ajax) read from rsxapp
during their initialization, before application code runs.
OBJECT STRUCTURE
Core Properties (always present):
build_key String. Manifest hash for cache-busting.
Changes when any source file changes.
session_hash String. Hashed session token for storage scoping.
Non-reversible hash of rsx cookie value.
debug Boolean. True in non-production environments.
current_controller String. PHP controller handling this request.
current_action String. Controller method name.
is_auth Boolean. True if user is logged in.
is_spa Boolean. True if page is SPA bootstrap.
params Object. URL parameters from route (e.g., {id: "4"}).
csrf String. CSRF token for form submissions.
Session Data (when authenticated):
user Object. Current user model with all fields.
Includes enum properties (role_id__label, etc.)
and __MODEL marker.
site Object. Current site model.
resolved_permissions Array. Pre-computed permission IDs for current user.
Includes role defaults plus supplementary grants,
minus supplementary denies. Empty array if not
authenticated. Use Permission.has_permission() to check.
Time Synchronization:
server_time String. ISO 8601 UTC timestamp from server.
Used by Rsx_Time to correct client clock skew.
user_timezone String. IANA timezone (e.g., "America/Chicago").
Resolved: user preference > site > config > default.
Custom Data:
page_data Object. Data added via PageData::add().
Only present if data was added.
Debug Mode Only:
console_debug Object. Console debug configuration.
Controls console_debug() output filtering.
ajax_disable_batching Boolean. When true, Ajax calls bypass batching.
Optional:
flash_alerts Array. Pending flash messages to display.
Consumed by Server_Side_Flash component.
log_browser_errors Boolean. When true, JS errors logged to server.
EXAMPLE OUTPUT
Typical rsxapp object for authenticated SPA page:
window.rsxapp = {
"build_key": "72d8554b3a6a4382d9130707caff4009",
"session_hash": "9b8cdc5ebf5a3db1e88d400bafe1af06...",
"debug": true,
"current_controller": "Frontend_Spa_Controller",
"current_action": "index",
"is_auth": true,
"is_spa": true,
"params": {"id": "4"},
"user": {
"id": 1,
"first_name": "Test",
"last_name": "User",
"email": "test@example.com",
"role_id": 300,
"role_id__label": "Site Owner",
"__MODEL": "User_Model"
},
"site": {
"id": 1,
"name": "Test Site",
"timezone": "America/Chicago",
"__MODEL": "Site_Model"
},
"resolved_permissions": [2, 3, 4, 5, 6, 7],
"csrf": "f290180b609f8f353c3226accdc798961...",
"page_data": {
"contact_internal_id": 17
},
"server_time": "2026-01-13T07:48:11.482Z",
"user_timezone": "America/Chicago"
};
COMMON USAGE PATTERNS
Check authentication:
if (window.rsxapp.is_auth) {
// User is logged in
}
Access current user:
const user_name = window.rsxapp.user.first_name;
const is_admin = window.rsxapp.user.role_id === User_Model.ROLE_SITE_OWNER;
Check permissions (use Permission class):
if (Permission.has_permission(User_Model.PERM_EDIT_DATA)) {
// User can edit data
}
if (Permission.has_any_permission([User_Model.PERM_EDIT_DATA, User_Model.PERM_VIEW_DATA])) {
// User can edit or view data
}
Read page data:
const contact_id = window.rsxapp.page_data?.contact_internal_id;
Get CSRF token for forms:
$('form').append(`<input type="hidden" name="_token" value="${window.rsxapp.csrf}">`);
Check if SPA mode:
if (window.rsxapp.is_spa) {
// Use client-side navigation
Spa.dispatch('/new-route');
}
ADDING CUSTOM DATA
Use PageData::add() in PHP to add custom data to page_data:
PageData::add([
'feature_flags' => ['new_ui', 'beta_feature'],
'config' => $site_config,
]);
Access in JavaScript:
if (window.rsxapp.page_data.feature_flags.includes('new_ui')) {
// Enable new UI
}
See pagedata(3) for detailed usage.
UNDERSCORE KEY FILTERING
Keys starting with single underscore (e.g., _internal) are automatically
filtered out before JSON encoding. Keys with double underscore (e.g., __MODEL)
are preserved. This allows models to have internal properties that don't
leak to JavaScript.
FRAMEWORK CONSUMERS
These framework classes read from rsxapp during initialization:
Rsx_Time Reads server_time and user_timezone for clock sync
Rsx_Storage Reads session_hash, user.id, site.id, build_key for scoping
Ajax Reads csrf for request headers
Spa Reads is_spa, params for routing
Permission Reads resolved_permissions for access control checks
Debugger Reads console_debug for output filtering
SEE ALSO
pagedata(3), bundle_api(3), time(3), storage(3), spa(3), acls(3)
AUTHOR
RSpade Framework
RSpade January 2026 RSXAPP(3)

View File

@@ -158,6 +158,32 @@ BOOTSTRAP CONTROLLER
- One per feature/bundle
- Naming: {Feature}_Spa_Controller::index
Passing Page Data:
Use PageData::add() to pass server-side data to JavaScript actions:
use App\RSpade\Core\View\PageData;
#[SPA]
public static function index(Request $request, array $params = [])
{
// Load data needed by SPA actions/components
$internal_contact = Contact_Model::where('type_id', Contact_Model::TYPE_INTERNAL)
->first();
PageData::add([
'contact_internal_id' => $internal_contact?->id,
'feature_flags' => config('rsx.features'),
]);
return rsx_view(SPA, ['bundle' => 'Frontend_Bundle']);
}
Access in JavaScript via window.rsxapp.page_data:
const internal_id = window.rsxapp.page_data.contact_internal_id;
See pagedata(3) for detailed usage.
Multiple SPA Bootstraps:
Different features can have separate SPA bootstraps:
- /app/frontend/Frontend_Spa_Controller::index (regular users)
@@ -899,4 +925,6 @@ SEE ALSO
routing(3) - URL generation and route patterns
modals(3) - Modal dialogs in SPA context
ajax_error_handling(3) - Error handling patterns
pagedata(3) - Passing server-side data to JavaScript
rsxapp(3) - Global JavaScript runtime object
scss(3) - SCSS scoping conventions and component-first philosophy