Move small tasks from wishlist to todo, update npm packages Replace #[Auth] attributes with manual auth checks and code quality rule Remove on_jqhtml_ready lifecycle method from framework Complete ACL system with 100-based role indexing and /dev/acl tester WIP: ACL system implementation with debug instrumentation Convert rsx:check JS linting to RPC socket server Clean up docs and fix $id→$sid in man pages, remove SSR/FPC feature Reorganize wishlists: priority order, mark sublayouts complete, add email Update model_fetch docs: mark MVP complete, fix enum docs, reorganize Comprehensive documentation overhaul: clarity, compression, and critical rules Convert Contacts/Projects CRUD to Model.fetch() and add fetch_or_null() Add JS ORM relationship lazy-loading and fetch array handling Add JS ORM relationship fetching and CRUD documentation Fix ORM hydration and add IDE resolution for Base_* model stubs Rename Json_Tree_Component to JS_Tree_Debug_Component and move to framework Enhance JS ORM infrastructure and add Json_Tree class name badges 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1407 lines
51 KiB
Plaintext
Executable File
1407 lines
51 KiB
Plaintext
Executable File
NAME
|
|
auth - RSX authentication, user invitations, and multi-tenant architecture
|
|
|
|
SYNOPSIS
|
|
Modern authentication system with Slack-style invitations, multi-tenant
|
|
support, and flexible session management for B2B SaaS applications
|
|
|
|
DESCRIPTION
|
|
The RSX authentication system provides a complete user management solution
|
|
built around two fundamental concepts:
|
|
|
|
1. Login Identity (login_users table)
|
|
- One email, one password, one identity
|
|
- Global authentication credentials
|
|
- Verified through invitation acceptance or signup
|
|
|
|
2. Site Membership (users table)
|
|
- Multiple memberships per login identity
|
|
- Each membership has its own role, name display
|
|
- Invitation workflow manages membership lifecycle
|
|
|
|
This separation enables:
|
|
- One person can be a member of multiple organizations/sites
|
|
- Each site membership has independent roles and profile data
|
|
- Invitation-based onboarding with email verification
|
|
- Works seamlessly for single-site or multi-tenant applications
|
|
|
|
CORE ARCHITECTURE
|
|
|
|
Database Tables:
|
|
|
|
login_users
|
|
id Login identity primary key
|
|
email Authentication email (unique)
|
|
password Hashed password
|
|
is_activated Account activation status
|
|
is_verified Email verification status
|
|
last_login Last successful login timestamp
|
|
created_at Account creation timestamp
|
|
updated_at Last update timestamp
|
|
|
|
users
|
|
id Site membership primary key
|
|
login_user_id FK to login_users (NULL until invite accepted)
|
|
site_id FK to sites
|
|
email Email for invitation (before login_user exists)
|
|
first_name Display name (part 1)
|
|
last_name Display name (part 2)
|
|
phone Contact number
|
|
role_id Site-specific role (1=OWNER, 2=ADMIN, 3=MEMBER)
|
|
is_enabled Active membership flag
|
|
invite_code Unique code for invitation URL
|
|
invite_accepted_at NULL until user accepts (marks as active member)
|
|
invite_expires_at Invitation expiration (default 7 days)
|
|
created_at Invitation sent timestamp
|
|
updated_at Last update timestamp
|
|
deleted_at Soft delete timestamp
|
|
|
|
sites
|
|
id Site/tenant primary key
|
|
name Organization name
|
|
slug URL-safe identifier
|
|
is_enabled Active site flag
|
|
|
|
sessions
|
|
session_token Cookie value (unique identifier)
|
|
login_user_id FK to login_users (NULL for anonymous)
|
|
site_id Current site context (NULL until selected)
|
|
experience_id Authentication realm (default 0)
|
|
active Session validity flag
|
|
expires_at Session expiration timestamp
|
|
updated_at Last activity timestamp
|
|
|
|
Relationships:
|
|
|
|
One login_user → Many users (site memberships)
|
|
One site → Many users (members)
|
|
Unique constraint: (login_user_id, site_id)
|
|
|
|
Sessions reference login_user_id, not users.id
|
|
Site selection sets session.site_id
|
|
|
|
Key Principles:
|
|
|
|
Invitation = Site Membership Record
|
|
No separate invites table. The users record IS the invitation
|
|
in a pending state (invite_accepted_at = NULL, login_user_id = NULL)
|
|
|
|
Lazy Session Creation
|
|
Sessions not created for every page view
|
|
Only created when: login, Session::id() called, CSRF needed
|
|
Anonymous browsing does not consume session resources
|
|
|
|
Email Verification via Invitation
|
|
Clicking invite link verifies email ownership
|
|
New accounts created via invite are auto-verified
|
|
No separate email verification flow needed
|
|
|
|
EMAIL BINDING AND SECURITY
|
|
|
|
Overview:
|
|
RSX invitations are bound to specific email addresses for enterprise
|
|
security. An invitation sent to john@company.com can ONLY be accepted
|
|
by creating or logging into an account with that exact email address.
|
|
|
|
Why Email Binding:
|
|
This design reflects how enterprise SaaS applications work in practice:
|
|
|
|
Security Model:
|
|
- When IT admin invites "john@company.com", they expect THAT
|
|
person to access the account
|
|
- Cannot forward invite link to someone else to hijack access
|
|
- Audit trail integrity: Actions tied to corporate identity
|
|
- Compliance: User actions must be attributable to verified identity
|
|
|
|
Deprovisioning:
|
|
- When employee leaves and email is disabled, they're locked out
|
|
- Works correctly with enterprise SSO and identity management
|
|
- No backdoor access via personal email accounts
|
|
|
|
Enterprise Control:
|
|
- Users cannot escape corporate control by changing email
|
|
- Admin controls which corporate identities have access
|
|
- Prevents scenarios like changing from corporate@company.com
|
|
to personal@gmail.com
|
|
|
|
Email Matching Rules:
|
|
1. Invitation created with email = "user@company.com"
|
|
2. Invitation can be accepted by:
|
|
- Creating NEW login account with email = "user@company.com"
|
|
- Logging into EXISTING account with email = "user@company.com"
|
|
3. Invitation CANNOT be accepted by:
|
|
- Different email address (even if same person)
|
|
- User must logout and login/signup with correct email
|
|
|
|
Email Mismatch Handling:
|
|
When logged in user tries to accept invite for different email:
|
|
|
|
System Shows:
|
|
"Email Mismatch
|
|
|
|
This invitation was sent to: user@company.com
|
|
You are currently logged in as: other@example.com
|
|
|
|
To accept this invitation, you must logout and login or create
|
|
an account using the invited email address.
|
|
|
|
[Logout and Continue]
|
|
|
|
If you believe this is an error, contact your site administrator
|
|
to request the invitation be resent to your current email address."
|
|
|
|
Email Changes (Admin-Controlled):
|
|
Users CANNOT change their login email address. Only administrators
|
|
can update email addresses to maintain corporate identity control.
|
|
|
|
Legitimate scenarios for admin email changes:
|
|
- Company domain migration (company.com → newcompany.com)
|
|
- Legal name changes (jsmith@co.com → jjones@co.com)
|
|
- Typo corrections by admin
|
|
- Organizational mergers and acquisitions
|
|
|
|
When admin changes user email:
|
|
1. Update login_users.email (authentication identity)
|
|
2. Update all users.email records for that login_user_id
|
|
3. Log change in audit trail (future feature)
|
|
4. Notify user of email change (future feature)
|
|
|
|
Email change process:
|
|
- Admin-only UI for email management (future feature)
|
|
- Bulk domain migration tool (future feature)
|
|
- Audit log for compliance (future feature)
|
|
|
|
Override for Non-Enterprise Apps:
|
|
RSpade provides this as the default for 90% of enterprise SaaS use
|
|
cases. Developers building consumer apps or flexible-identity systems
|
|
can override this behavior since all authentication code is exposed
|
|
in /rsx/app/login/ as template application code, not locked in
|
|
the framework.
|
|
|
|
INVITATION WORKFLOW
|
|
|
|
Overview:
|
|
RSX uses Slack-style invitations where site admins invite users by
|
|
email. The invitation creates a pending users record bound to that
|
|
email address. When accepted, the user either creates a login account
|
|
or links their existing one, but ONLY if the email addresses match.
|
|
|
|
Multi-Account Philosophy:
|
|
RSX does NOT implement Google/Reddit-style multi-identity account
|
|
switching (switching between different email addresses in same browser).
|
|
|
|
Why no multi-identity switcher:
|
|
- People naturally segment by device/browser (work computer vs home)
|
|
- Original driver was multi-site access (one email, many sites)
|
|
- That problem is solved by current multi-site architecture
|
|
- Multi-identity switcher adds cognitive overhead
|
|
- Rare real-world need (users use browser profiles instead)
|
|
|
|
What RSX DOES support:
|
|
- Multi-site: One email address can be a member of multiple sites
|
|
- Site picker: User selects which site to access after login
|
|
- Clean separation: Different logins use different browser sessions
|
|
|
|
If users need multiple identities: Use different browsers/profiles,
|
|
or logout/login. This is standard across most B2B SaaS products.
|
|
|
|
Smart Invitation Acceptance:
|
|
When user clicks an invitation link, RSX auto-detects their account
|
|
state and shows the appropriate next action.
|
|
|
|
Why auto-detect instead of "Create Account OR Login" choice:
|
|
- Modern SaaS products (Slack, Teams, GitHub) auto-detect
|
|
- System knows if account exists for invited email
|
|
- Showing one correct action is less confusing than both options
|
|
- Email enumeration is not a security concern in invite context
|
|
(invitation itself proves we know the email address)
|
|
|
|
Acceptance Flow States:
|
|
1. Not logged in + No account exists
|
|
→ Show "Create Account" button
|
|
→ Small text: "Already have account? Sign in instead"
|
|
|
|
2. Not logged in + Account exists
|
|
→ Show "Sign In to Accept Invitation" button
|
|
→ Small text: "Not you? Use different account"
|
|
|
|
3. Logged in + Email matches
|
|
→ Show "Accept Invitation" button
|
|
→ User continues immediately
|
|
|
|
4. Logged in + Email mismatch
|
|
→ Show email mismatch error
|
|
→ Logout and continue option
|
|
|
|
5. Already accepted
|
|
→ Show "Go to Dashboard" button
|
|
|
|
Smart detection provides obvious next steps that match user's actual
|
|
state, reducing confusion and support requests.
|
|
|
|
1. Admin Invites User
|
|
|
|
Admin Action:
|
|
- Opens "Add User" modal from user management page
|
|
- Enters: email (required), first/last name (optional), role, phone
|
|
- Submits form
|
|
|
|
System Processing:
|
|
1. Verify email not already a member of this site
|
|
2. Create users record:
|
|
- email (for invitation)
|
|
- first_name, last_name, phone, role_id (from form)
|
|
- login_user_id = NULL (not linked yet)
|
|
- invite_code = generate_unique_code()
|
|
- invite_accepted_at = NULL (pending)
|
|
- invite_expires_at = now + 7 days (configurable)
|
|
3. TODO: Send invitation email to email address
|
|
Subject: "You've been invited to join [Site Name]"
|
|
Link: /accept-invite/{invite_code}
|
|
|
|
Result:
|
|
Pending membership created, invitation email sent
|
|
|
|
2. New User Accepts Invite (No Account)
|
|
|
|
User Action:
|
|
- Clicks /accept-invite/{code} from email
|
|
- Not logged in, no existing account
|
|
|
|
System Shows:
|
|
"You've been invited to join [Site Name]!
|
|
|
|
Create an account to get started:
|
|
Email: [email from invite] (pre-filled, read-only)
|
|
First Name: [from invite or blank]
|
|
Last Name: [from invite or blank]
|
|
Phone: [from invite or blank]
|
|
Password: [________]
|
|
Confirm Password: [________]
|
|
|
|
[Create Account & Accept Invite]
|
|
|
|
Already have an account? [Login instead]"
|
|
|
|
Validation:
|
|
- Email must match invitation email EXACTLY
|
|
- Email field is pre-filled and read-only (cannot be changed)
|
|
- Password minimum 8 characters
|
|
- Confirm password must match
|
|
|
|
On Account Creation:
|
|
1. Verify email matches invitation email
|
|
2. Create login_users record:
|
|
- email (from invite, verified to match)
|
|
- password (hashed)
|
|
- is_activated = true (auto-activate on invite)
|
|
- is_verified = true (email verified by invite click)
|
|
3. Link membership to login identity:
|
|
- users.login_user_id = login_user.id
|
|
- users.invite_accepted_at = now()
|
|
4. Log user in:
|
|
- Session::set_user(login_user)
|
|
- Session::set_site_id(users.site_id)
|
|
5. Redirect to /dashboard
|
|
|
|
Result:
|
|
New login_users record created with matching email, membership
|
|
linked and accepted, user logged in and redirected to site
|
|
|
|
3. Existing User Accepts Invite (Has Account)
|
|
|
|
3A. User Logged In, Email Matches
|
|
|
|
User Action:
|
|
- Clicks /accept-invite/{code} from email
|
|
- Already logged in with same email
|
|
|
|
Validation:
|
|
- Check logged in user email matches invitation email
|
|
- If match → proceed with acceptance
|
|
- If no match → show email mismatch error (see 3B below)
|
|
|
|
System Shows:
|
|
"Welcome back!
|
|
|
|
You've been invited to join [Site Name].
|
|
Click to accept and get started.
|
|
|
|
[Accept Invitation]"
|
|
|
|
On Acceptance:
|
|
1. Verify email match one more time:
|
|
- login_user.email === invitation.email
|
|
2. Link membership:
|
|
- users.login_user_id = current login_user.id
|
|
- users.invite_accepted_at = now()
|
|
3. Set site context:
|
|
- Session::set_site_id(users.site_id)
|
|
4. Redirect to /dashboard
|
|
|
|
Result:
|
|
Membership linked and accepted, user continues to site
|
|
|
|
3B. User Logged In, Email Mismatch
|
|
|
|
User Action:
|
|
- Clicks /accept-invite/{code} from email
|
|
- Logged in with DIFFERENT email
|
|
|
|
Email Check:
|
|
- Compare login_user.email with invitation.email
|
|
- If mismatch → Block acceptance and show error
|
|
|
|
System Shows:
|
|
"Email Mismatch
|
|
|
|
This invitation was sent to: [invite email]
|
|
You are currently logged in as: [current email]
|
|
|
|
To accept this invitation, you must be logged in with the
|
|
invited email address. This security measure ensures
|
|
invitations are accepted by the intended recipient.
|
|
|
|
[Logout and Continue]
|
|
|
|
If you believe this is an error, contact your site
|
|
administrator to request the invitation be updated or
|
|
resent to your current email address."
|
|
|
|
On "Logout and Continue":
|
|
1. Session::logout()
|
|
2. Redirect back to /accept-invite/{code}
|
|
3. User sees "not logged in" flow (section 3C)
|
|
4. Can now create account or login with correct email
|
|
|
|
Important:
|
|
- No "accept anyway" option (security requirement)
|
|
- Cannot bypass email matching
|
|
- Admin must update invitation email if legitimately wrong
|
|
- Prevents invite hijacking and unauthorized access
|
|
|
|
Result:
|
|
User must logout and use correct email to accept invitation
|
|
|
|
3C. User Not Logged In
|
|
|
|
User Action:
|
|
- Clicks /accept-invite/{code} from email
|
|
- Not logged in
|
|
|
|
System Shows:
|
|
"You've been invited to join [Site Name]!
|
|
|
|
Email: [invite email] (pre-filled)
|
|
Password: [________]
|
|
|
|
[Login & Accept Invite]
|
|
|
|
Don't have an account? [Create one]"
|
|
|
|
On Login:
|
|
- Authenticate login_users with email/password
|
|
- Check if email matches invite email
|
|
- If match → Same as 3A flow
|
|
- If mismatch → Same as 3B flow
|
|
|
|
On "Create one":
|
|
- Same as "New User Accepts Invite" flow (section 2)
|
|
|
|
Result:
|
|
User logs in or creates account, then follows appropriate flow
|
|
|
|
4. User Logs In (Regular Login Flow)
|
|
|
|
User Action:
|
|
- Navigates to /login
|
|
- Enters email and password
|
|
|
|
Authentication:
|
|
1. Find login_user by email
|
|
2. Verify password with password_verify()
|
|
3. If invalid, show error
|
|
4. If valid, create session:
|
|
- Session::set_user(login_user)
|
|
|
|
Site Selection Logic:
|
|
1. Query accepted memberships:
|
|
- WHERE login_user_id = current user
|
|
- AND invite_accepted_at IS NOT NULL
|
|
- AND invite_expires_at > now()
|
|
- AND deleted_at IS NULL
|
|
|
|
2. Count memberships:
|
|
|
|
Zero sites:
|
|
- Session::logout()
|
|
- Error: "You do not have access to any sites.
|
|
Contact your administrator."
|
|
- Stays on login page
|
|
|
|
One site:
|
|
- Session::set_site_id(site_id)
|
|
- Redirect to /dashboard
|
|
|
|
Multiple sites:
|
|
- Show site picker modal
|
|
- User clicks site name
|
|
- Session::set_site_id(selected_site_id)
|
|
- Redirect to /dashboard
|
|
|
|
First-Login Auto-Accept (Special Case):
|
|
After authentication, before site selection, check for pending
|
|
invites matching user's email:
|
|
|
|
1. Query pending invites:
|
|
- WHERE email = login_user.email
|
|
- AND login_user_id IS NULL
|
|
- AND invite_accepted_at IS NULL
|
|
- AND invite_expires_at > now()
|
|
|
|
2. If found:
|
|
- Show acceptance modal:
|
|
"You have a pending invitation to [Site Name].
|
|
Click to accept and get started."
|
|
- On accept:
|
|
- Link: invite.login_user_id = login_user.id
|
|
- Mark: invite.invite_accepted_at = now()
|
|
- Continue to normal site selection logic
|
|
|
|
IMPORTANT: This only happens on first login after invite sent.
|
|
Subsequent invites MUST be accepted via email link for security.
|
|
|
|
Result:
|
|
User authenticated and directed to appropriate site dashboard
|
|
|
|
SIGNUP WORKFLOW
|
|
|
|
Overview:
|
|
RSpade supports three signup modes configured in rsx/resource/config/rsx.php:
|
|
- anonymous: Anyone can create an account
|
|
- invite_only: Only invited users can sign up
|
|
- disabled: No new signups allowed (closed system)
|
|
|
|
Configuration:
|
|
File: rsx/resource/config/rsx.php
|
|
|
|
return [
|
|
// Signup mode
|
|
'signup_mode' => 'invite_only', // 'anonymous', 'invite_only', 'disabled'
|
|
|
|
// Verification requirements
|
|
'verification_required' => 'email', // 'email', 'sms', 'either', 'both'
|
|
|
|
// Invitation expiration (days)
|
|
'invite_expiration_days' => 7,
|
|
];
|
|
|
|
Configuration Philosophy:
|
|
These settings are provided as developer convenience for rapid testing.
|
|
They allow quickly switching between behaviors during development.
|
|
|
|
For production applications, developers should:
|
|
1. Determine which mode their application needs
|
|
2. Find the code using these config settings
|
|
3. Remove the switches and hardcode desired behavior
|
|
4. Customize the implementation to their exact needs
|
|
|
|
This keeps code simple and prevents "configuration explosion" where
|
|
every feature has multiple modes to maintain.
|
|
|
|
Signup Mode: anonymous
|
|
|
|
Behavior:
|
|
- Public /signup page available
|
|
- Anyone can create account without invitation
|
|
- Account created immediately
|
|
- Verification required based on verification_required setting
|
|
- User automatically added to site_id = 1 with role MEMBER
|
|
|
|
Use Cases:
|
|
- Public SaaS applications
|
|
- Marketing sites with trial signups
|
|
- Community platforms
|
|
- Any application where anyone can join
|
|
|
|
Workflow:
|
|
1. User visits /signup
|
|
2. Fills form: email, password, first/last name (optional)
|
|
3. Submits form
|
|
4. System creates login_users record (is_verified = false)
|
|
5. System creates users record (site_id = 1, role_id = 3 MEMBER)
|
|
6. Users record immediately accepted (invite_accepted_at = now)
|
|
7. If verification required:
|
|
- Send verification code via email/SMS
|
|
- Show verification modal (PIN entry)
|
|
- User enters code, account activated
|
|
- Redirect to dashboard
|
|
8. If verification not required:
|
|
- Log user in immediately
|
|
- Redirect to dashboard
|
|
|
|
Security Considerations:
|
|
- Rate limit signup endpoint (prevent spam accounts)
|
|
- CAPTCHA for anonymous signups (prevent bots)
|
|
- Email verification strongly recommended
|
|
- Monitor for abuse patterns
|
|
|
|
Signup Mode: invite_only
|
|
|
|
Behavior:
|
|
- /signup page requires invite code in URL: /signup?code={invite_code}
|
|
- Without valid invite code, redirects to login with error message
|
|
- Users can only create accounts via invitation acceptance
|
|
- Email must match invitation email (enforced)
|
|
- Invitation validates email ownership
|
|
|
|
Use Cases:
|
|
- Private beta applications
|
|
- Internal company tools
|
|
- B2B applications with controlled onboarding
|
|
- Default mode for RSpade starter template
|
|
|
|
Workflow:
|
|
See INVITATION WORKFLOW section above for complete details.
|
|
|
|
Summary:
|
|
1. Admin invites user → users record created (pending state)
|
|
2. User receives email with /accept-invite/:code link
|
|
3. User clicks link, system validates:
|
|
- Invitation exists
|
|
- Not expired (invite_expires_at > now)
|
|
- Not already accepted (invite_accepted_at IS NULL)
|
|
4. If not logged in: Show account creation form
|
|
- Email pre-filled from invitation (read-only)
|
|
- Email field cannot be changed (security)
|
|
- User sets password
|
|
5. If logged in with matching email: Show accept button
|
|
6. If logged in with different email: Show email mismatch error
|
|
7. Account created/linked, invitation accepted, user logged in
|
|
|
|
Email Matching Enforcement:
|
|
- Signup form email field is read-only (uses invitation email)
|
|
- Cannot create account with different email
|
|
- If user tries to accept with wrong logged-in account:
|
|
→ Show error and require logout
|
|
- Admin must update invitation if email legitimately wrong
|
|
|
|
Benefits:
|
|
- Email verified via invitation click
|
|
- Controlled user growth
|
|
- Know who invited each user
|
|
- Prevent spam signups
|
|
- Prevent invite hijacking (email matching requirement)
|
|
|
|
Signup Mode: disabled
|
|
|
|
Behavior:
|
|
- No signup page available
|
|
- /signup redirects to /login with error message
|
|
- No invitation acceptance allowed
|
|
- Accounts must be created manually by administrators
|
|
|
|
Use Cases:
|
|
- Closed systems (all users pre-created)
|
|
- Applications in maintenance mode
|
|
- Internal tools with fixed user list
|
|
- Temporarily disable signups during issues
|
|
|
|
Workflow:
|
|
Admin must manually create accounts:
|
|
1. Admin creates login_users record directly
|
|
2. Admin creates users record for site membership
|
|
3. Admin provides credentials to user separately
|
|
4. User logs in with provided credentials
|
|
|
|
Implementation:
|
|
Routes /signup and /accept-invite return 403 or redirect with:
|
|
"New account signups are currently disabled. Contact your
|
|
administrator for assistance."
|
|
|
|
Verification Methods
|
|
|
|
Email Verification:
|
|
1. On signup, generate random 6-digit PIN
|
|
2. Store in verification table with expiration
|
|
3. Send email with PIN code
|
|
4. Show modal: "Enter verification code sent to your email"
|
|
5. User enters PIN
|
|
6. Verify PIN matches and not expired
|
|
7. Mark login_user.is_verified = true
|
|
8. Delete verification record
|
|
9. Complete login
|
|
|
|
SMS Verification:
|
|
Similar to email, but:
|
|
- Require phone number during signup
|
|
- Send PIN via SMS service (Twilio, etc.)
|
|
- User enters PIN from text message
|
|
|
|
Either Verification:
|
|
- User chooses email OR SMS during signup
|
|
- Provide both options in UI
|
|
- Complete either one to verify account
|
|
|
|
Both Verification:
|
|
- Require BOTH email AND SMS verification
|
|
- Must complete both before account activated
|
|
- Higher security for sensitive applications
|
|
|
|
Verification UI:
|
|
RSpade includes a PIN verification modal component.
|
|
See: /rsx/theme/components/verification_pin_modal.jqhtml
|
|
|
|
Anonymous Signup Implementation
|
|
|
|
Route: /signup (GET)
|
|
Show signup form:
|
|
- Email (required, validated)
|
|
- Password (required, min 8 characters)
|
|
- Confirm Password (must match)
|
|
- First Name (optional)
|
|
- Last Name (optional)
|
|
- Phone (if SMS verification required)
|
|
- Terms acceptance checkbox
|
|
- "Already have an account? Login" link
|
|
|
|
Route: /signup (POST)
|
|
Process signup:
|
|
1. Validate input (email unique, password strength)
|
|
2. Check signup_mode = 'anonymous' (else error)
|
|
3. Create login_users record:
|
|
- email
|
|
- password (hashed)
|
|
- is_activated = true
|
|
- is_verified = false (until verification)
|
|
4. Create users record:
|
|
- login_user_id
|
|
- site_id = 1 (default site)
|
|
- role_id = 3 (MEMBER)
|
|
- email (same as login_users)
|
|
- first_name, last_name, phone
|
|
- invite_accepted_at = now() (not an invite, auto-accepted)
|
|
5. If verification required:
|
|
- Generate PIN, store with expiration
|
|
- Send via email/SMS
|
|
- Show verification modal
|
|
- Return (don't log in yet)
|
|
6. If no verification required:
|
|
- Session::set_user(login_user)
|
|
- Session::set_site_id(1)
|
|
- Redirect to /dashboard
|
|
|
|
Route: /verify-account (POST)
|
|
Verify PIN code:
|
|
1. Get PIN from input
|
|
2. Find verification record for user
|
|
3. Check PIN matches and not expired
|
|
4. Mark login_user.is_verified = true
|
|
5. Delete verification record
|
|
6. Session::set_user(login_user)
|
|
7. Session::set_site_id(1)
|
|
8. Redirect to /dashboard
|
|
|
|
API REFERENCE
|
|
|
|
RsxAuth Facade:
|
|
|
|
RsxAuth::check(): bool
|
|
Returns true if user is logged in
|
|
|
|
RsxAuth::user(): ?Login_User_Model
|
|
Returns current login_user instance or null
|
|
|
|
RsxAuth::id(): ?int
|
|
Returns current login_user_id or null
|
|
|
|
RsxAuth::session(): ?Session
|
|
Returns current session instance or null
|
|
|
|
RsxAuth::login(Login_User_Model $user): void
|
|
Authenticate user and create session
|
|
|
|
RsxAuth::logout(): void
|
|
Clear session and log user out
|
|
|
|
Session Methods:
|
|
|
|
Session::init(): void
|
|
Initialize session for current request
|
|
Loads session by token + experience_id
|
|
Called automatically by framework
|
|
|
|
Session::set_user(Login_User_Model $user): void
|
|
Set logged in user for session
|
|
Updates session.login_user_id
|
|
Persists to database
|
|
|
|
Session::get_user(): ?Login_User_Model
|
|
Get current logged in user
|
|
Returns null if not logged in
|
|
|
|
Session::get_user_id(): ?int
|
|
Get current login_user_id
|
|
Returns null if not logged in
|
|
|
|
Session::set_site_id(int $site_id): void
|
|
Set current site context for session
|
|
Updates session.site_id
|
|
Persists to database
|
|
|
|
Session::get_site_id(): int
|
|
Get current site_id
|
|
Returns 0 if no site selected
|
|
|
|
Session::get_site(): ?Site_Model
|
|
Get current site model instance
|
|
Returns null if no site selected
|
|
|
|
Session::logout(): void
|
|
Clear session and destroy token
|
|
Redirects to login page
|
|
|
|
Session::id(): string
|
|
Get session token (creates session if needed)
|
|
Use for anonymous tracking (e.g., file uploads)
|
|
|
|
Experience Methods (Advanced):
|
|
|
|
Session::set_experience_id(int $experience_id): void
|
|
Set authentication realm for request
|
|
Used for customer portals vs staff panels
|
|
Default 0 for standard applications
|
|
See MULTI-REALM AUTHENTICATION section
|
|
|
|
Session::get_experience_id(): int
|
|
Get current experience_id
|
|
Default 0
|
|
|
|
Request Override Methods (Advanced):
|
|
|
|
Session::set_request_site_id_override(int $site_id): void
|
|
Force site_id for current request only
|
|
Does NOT persist to session record
|
|
Used for subdomain-based tenant enforcement
|
|
See SUBDOMAIN-BASED MULTI-TENANT section
|
|
|
|
Session::clear_request_site_id_override(): void
|
|
Remove request-scoped site override
|
|
|
|
Session::has_request_site_id_override(): bool
|
|
Check if request override is active
|
|
|
|
CONTROLLER PATTERNS
|
|
|
|
Authentication Philosophy:
|
|
|
|
RSX uses manual authentication checks rather than declarative attributes.
|
|
This approach ensures developers explicitly handle auth at the code level,
|
|
making permission logic visible and traceable in the actual method code.
|
|
|
|
A code quality rule (PHP-AUTH-01) verifies that all endpoints have auth
|
|
checks, either in the method body or in the controller's pre_dispatch().
|
|
|
|
Controller-Level Authentication (Recommended):
|
|
|
|
Add auth check to pre_dispatch() to protect all endpoints in a controller:
|
|
|
|
class My_Controller extends Rsx_Controller_Abstract
|
|
{
|
|
public static function pre_dispatch(Request $request, array $params = [])
|
|
{
|
|
if (!Session::is_logged_in()) {
|
|
return response_unauthorized();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
#[Route('/dashboard')]
|
|
public static function dashboard(Request $request, array $params = [])
|
|
{
|
|
// User guaranteed to be logged in (checked in pre_dispatch)
|
|
$user = Session::get_user();
|
|
}
|
|
|
|
#[Ajax_Endpoint]
|
|
public static function save_data(Request $request, array $params = [])
|
|
{
|
|
// Also protected by pre_dispatch
|
|
}
|
|
}
|
|
|
|
Method-Level Authentication:
|
|
|
|
Add auth check at the start of individual methods:
|
|
|
|
#[Ajax_Endpoint]
|
|
public static function save_settings(Request $request, array $params = [])
|
|
{
|
|
if (!Session::is_logged_in()) {
|
|
return response_unauthorized();
|
|
}
|
|
|
|
// Process request
|
|
}
|
|
|
|
Response Helpers:
|
|
|
|
response_unauthorized(?string $message = null)
|
|
Returns context-aware unauthorized response:
|
|
- Ajax: JSON {success: false, error_code: 'unauthorized'}
|
|
- Web: Redirect to login or render 403 page
|
|
|
|
response_not_found(?string $message = null)
|
|
Returns context-aware not found response:
|
|
- Ajax: JSON {success: false, error_code: 'not_found'}
|
|
- Web: Render 404 page
|
|
|
|
Permission Checks:
|
|
|
|
Use Permission class helpers for role/permission-based access:
|
|
|
|
if (!Permission::has_permission(User_Model::PERM_EDIT_DATA)) {
|
|
return response_unauthorized('Insufficient permissions');
|
|
}
|
|
|
|
if (!Permission::has_role(User_Model::ROLE_MANAGER)) {
|
|
return response_unauthorized('Manager access required');
|
|
}
|
|
|
|
Public Endpoints (Auth Exempt):
|
|
|
|
For public endpoints, add @auth-exempt comment to class docblock:
|
|
|
|
/**
|
|
* Login controller
|
|
*
|
|
* @auth-exempt Login routes are public by design
|
|
*/
|
|
class Login_Controller extends Rsx_Controller_Abstract
|
|
{
|
|
#[Route('/login')]
|
|
public static function index(Request $request, array $params = [])
|
|
{
|
|
// No auth check needed - marked exempt
|
|
}
|
|
}
|
|
|
|
Or exempt individual methods:
|
|
|
|
/**
|
|
* @auth-exempt Public webhook endpoint
|
|
*/
|
|
#[Ajax_Endpoint]
|
|
public static function webhook(Request $request, array $params = [])
|
|
{
|
|
// Process webhook
|
|
}
|
|
|
|
Code Quality Enforcement:
|
|
|
|
The PHP-AUTH-01 rule in rsx:check verifies:
|
|
- All #[Route], #[SPA], #[Ajax_Endpoint] methods have auth checks
|
|
- Check can be in pre_dispatch() OR method body
|
|
- @auth-exempt comment exempts from requirement
|
|
|
|
Run: php artisan rsx:check
|
|
|
|
SINGLE-SITE APPLICATIONS
|
|
|
|
Overview:
|
|
RSX is multi-tenant by default, but works perfectly for single-site
|
|
applications. Simply use site_id = 1 for everything.
|
|
|
|
Configuration:
|
|
No special configuration needed. Architecture is identical.
|
|
|
|
Behavior:
|
|
|
|
User Invitation:
|
|
- Admin invites user → users.site_id = 1
|
|
- User accepts → membership on site 1
|
|
|
|
Login Flow:
|
|
- User logs in → System finds 1 site membership
|
|
- Automatically sets session.site_id = 1
|
|
- No site picker shown (only one option)
|
|
|
|
User Removal:
|
|
- Remove user from site 1 → No site memberships
|
|
- Next login → "No site access" error
|
|
- User forced to logout
|
|
|
|
Code Access:
|
|
Current site ID: Session::get_site_id() → Always returns 1
|
|
Current site: Session::get_site() → Always returns site 1
|
|
|
|
Key Points:
|
|
- Invitation workflow identical to multi-tenant
|
|
- Site picker never shows (always exactly 1 site)
|
|
- User management CRUD same as multi-tenant
|
|
- No special single-site mode needed
|
|
- Easy upgrade path to multi-tenant if needed
|
|
|
|
MULTI-REALM AUTHENTICATION (ADVANCED)
|
|
|
|
Concept:
|
|
Experience = Authentication realm. One user can be logged into multiple
|
|
experiences simultaneously using the same cookie, with different
|
|
login_user_id per experience.
|
|
|
|
Use Cases:
|
|
- Customer portal (/portal/*) + Staff panel (/staff/*)
|
|
- Public site + Admin area with separate logins
|
|
- Multiple authentication contexts in one application
|
|
|
|
Database Structure:
|
|
sessions table has experience_id column (default 0)
|
|
Same session_token can have multiple rows with different experience_id
|
|
|
|
Implementation in Main.php:
|
|
|
|
public static function pre_dispatch(Request $request, array $params = [])
|
|
{
|
|
$path = $request->path();
|
|
|
|
if (str_starts_with($path, 'staff/')) {
|
|
Session::set_experience_id(1); // Staff realm
|
|
} elseif (str_starts_with($path, 'portal/')) {
|
|
Session::set_experience_id(2); // Customer realm
|
|
} else {
|
|
Session::set_experience_id(0); // Default realm
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
How It Works:
|
|
1. User visits /portal/dashboard → experience_id = 2
|
|
Session loads: WHERE token=X AND experience_id=2
|
|
User logged in as customer (login_user_id=123)
|
|
|
|
2. Same user visits /staff/admin → experience_id = 1
|
|
Session loads: WHERE token=X AND experience_id=1
|
|
User logged in as staff (login_user_id=456)
|
|
|
|
3. Both sessions active simultaneously with same cookie
|
|
|
|
Experience Methods:
|
|
Session::set_experience_id(int $id) Set realm for request
|
|
Session::get_experience_id() Get current realm
|
|
|
|
Important:
|
|
Most applications use default experience_id = 0
|
|
Only implement multi-realm if you need separate authentication
|
|
contexts within same application
|
|
|
|
SUBDOMAIN-BASED MULTI-TENANT (ADVANCED)
|
|
|
|
Concept:
|
|
Each site/tenant has their own subdomain. Site is determined by
|
|
subdomain, not user choice. Users cannot switch sites arbitrarily.
|
|
|
|
URL Structure:
|
|
acme.yourapp.com → site_id = 1 (Acme Corp)
|
|
widget.yourapp.com → site_id = 2 (Widget Inc)
|
|
|
|
Implementation in Main.php:
|
|
|
|
public static function pre_dispatch(Request $request, array $params = [])
|
|
{
|
|
$host = $request->getHost();
|
|
$subdomain = explode('.', $host)[0];
|
|
|
|
$site = Site_Model::where('slug', $subdomain)->first();
|
|
|
|
if (!$site) {
|
|
return response('Site not found', 404);
|
|
}
|
|
|
|
// Enforce site for current request (does NOT persist to session)
|
|
Session::set_request_site_id_override($site->id);
|
|
|
|
return null;
|
|
}
|
|
|
|
How It Works:
|
|
1. User visits acme.yourapp.com
|
|
2. Framework sets request override: site_id = 1
|
|
3. All code calling Session::get_site_id() gets site 1
|
|
4. Session record site_id remains null (not persisted)
|
|
5. Next request on widget.yourapp.com gets site 2
|
|
|
|
Benefits:
|
|
- No site picker needed (determined by URL)
|
|
- No session writes for site switching (performance)
|
|
- Clear tenant isolation via DNS
|
|
- Traditional "have account or don't" UX
|
|
|
|
Traditional Login Flow:
|
|
- User goes to acme.yourapp.com/login
|
|
- Subdomain determines site_id = 1
|
|
- User logs in
|
|
- System checks: Is user a member of site 1?
|
|
- If yes: Allow login, redirect to dashboard
|
|
- If no: Error "No access to this site"
|
|
|
|
This is a future planned feature. Current implementation uses
|
|
organization picker (Trello-style) by default.
|
|
|
|
CONFIGURATION
|
|
|
|
Configuration File: rsx/resource/config/rsx.php
|
|
|
|
return [
|
|
// Invitation expiration (days)
|
|
'invite_expiration_days' => 7,
|
|
|
|
// Session lifetime (minutes)
|
|
'session_lifetime' => 525600, // 365 days
|
|
|
|
// Session cookie name
|
|
'session_cookie_name' => 'rsx_session',
|
|
];
|
|
|
|
Usage in Code:
|
|
|
|
$days = config('rsx.invite_expiration_days', 7);
|
|
$expires_at = now()->addDays($days);
|
|
|
|
REQUIRED ROUTES
|
|
|
|
The following routes must be implemented for complete authentication:
|
|
|
|
Login/Logout:
|
|
/login Show login form
|
|
/login (POST) Process login
|
|
/logout Logout user
|
|
|
|
Invitation Acceptance:
|
|
/accept-invite/:code Show acceptance page
|
|
/accept-invite/:code/signup (POST)
|
|
Create account from invite
|
|
/accept-invite/:code/login (POST)
|
|
Login and accept invite
|
|
|
|
Site Selection (Multi-tenant):
|
|
/select-site Show site picker (multiple sites)
|
|
/select-site/:site_id (POST)
|
|
Set site and redirect
|
|
|
|
User Management (Admin):
|
|
/settings/user-management List users + Add User button
|
|
/settings/user-management/view/:id
|
|
View user details
|
|
|
|
Ajax Endpoints:
|
|
Frontend_Settings_User_Management_Controller::add_user()
|
|
Create invitation (modal form)
|
|
Frontend_Settings_User_Management_Controller::datagrid_fetch()
|
|
Load user list for DataGrid
|
|
|
|
SESSION LIFECYCLE
|
|
|
|
Session Creation:
|
|
Sessions are created lazily, not for every page view.
|
|
|
|
Created when:
|
|
- User logs in (Session::set_user())
|
|
- Session::id() explicitly called (e.g., file upload tracking)
|
|
- CSRF token requested (requires session)
|
|
|
|
NOT created for:
|
|
- Anonymous page views (public pages)
|
|
- Static asset requests (CSS, JS, images)
|
|
- Until explicitly needed by code
|
|
|
|
Session Fields:
|
|
session_token Unique identifier (stored in cookie)
|
|
login_user_id FK to login_users (NULL for anonymous)
|
|
site_id Current site context (NULL until selected)
|
|
experience_id Authentication realm (default 0)
|
|
active Session validity flag
|
|
expires_at Session expiration (default 365 days)
|
|
created_at Session creation timestamp
|
|
updated_at Last activity timestamp
|
|
|
|
Session States:
|
|
|
|
Anonymous Session:
|
|
- session_token exists
|
|
- login_user_id = NULL
|
|
- site_id = NULL
|
|
- Used for: File uploads, form CSRF, tracking
|
|
|
|
Authenticated Session (No Site):
|
|
- session_token exists
|
|
- login_user_id set
|
|
- site_id = NULL
|
|
- Occurs: After login, before site selection
|
|
|
|
Authenticated Session (With Site):
|
|
- session_token exists
|
|
- login_user_id set
|
|
- site_id set
|
|
- Normal working state for logged in users
|
|
|
|
Session Expiration:
|
|
Default lifetime: 365 days (configurable)
|
|
Updated on every request
|
|
Expired sessions automatically cleaned up
|
|
|
|
Important Notes:
|
|
- Session cookie ≠ Logged in user
|
|
- RsxAuth::check() only true if login_user_id set
|
|
- Session persists across login/logout
|
|
- Session token can outlive login status
|
|
|
|
SECURITY CONSIDERATIONS
|
|
|
|
Password Storage:
|
|
- Use password_hash() with bcrypt (PHP default)
|
|
- Never store plaintext passwords
|
|
- Verify with password_verify()
|
|
|
|
Email Verification:
|
|
- Invitation click verifies email ownership
|
|
- New accounts via invite auto-verified
|
|
- is_verified = true on invitation acceptance
|
|
|
|
Invitation Security:
|
|
- Unique random invite codes (generate_unique_code())
|
|
- Expiration enforced (default 7 days)
|
|
- First-login auto-accept only on first login
|
|
- Subsequent invites require email link click
|
|
- Email mismatch protection on acceptance
|
|
|
|
Session Security:
|
|
- Unique session tokens per session
|
|
- Token stored in httponly cookie
|
|
- Session expiration enforced
|
|
- Active flag for forced logout
|
|
|
|
CSRF Protection:
|
|
- All POST routes require CSRF token
|
|
- Token tied to session
|
|
- Automatic validation by framework
|
|
|
|
EXAMPLES
|
|
|
|
Example 1: Check if User is Logged In
|
|
|
|
if (RsxAuth::check()) {
|
|
$user = RsxAuth::user();
|
|
echo "Welcome, {$user->email}!";
|
|
} else {
|
|
echo "Please log in.";
|
|
}
|
|
|
|
Example 2: Get Current Site Membership
|
|
|
|
$login_user_id = RsxAuth::id();
|
|
$site_id = Session::get_site_id();
|
|
|
|
$membership = User_Model::where('login_user_id', $login_user_id)
|
|
->where('site_id', $site_id)
|
|
->first();
|
|
|
|
if ($membership) {
|
|
echo "Role: {$membership->role_id}";
|
|
echo "Name: {$membership->first_name} {$membership->last_name}";
|
|
}
|
|
|
|
Example 3: Create User Invitation
|
|
|
|
$user = new User_Model();
|
|
$user->site_id = Session::get_site_id();
|
|
$user->email = $request->input('email');
|
|
$user->first_name = $request->input('first_name');
|
|
$user->last_name = $request->input('last_name');
|
|
$user->role_id = $request->input('role_id');
|
|
$user->login_user_id = null; // Not linked yet
|
|
$user->invite_code = Str::random(32);
|
|
$user->invite_accepted_at = null;
|
|
$user->invite_expires_at = now()->addDays(
|
|
config('rsx.invite_expiration_days', 7)
|
|
);
|
|
$user->save();
|
|
|
|
// TODO: Send invitation email
|
|
// $url = url('/accept-invite/' . $user->invite_code);
|
|
// Mail::to($user->email)->send(new InvitationEmail($url));
|
|
|
|
Example 4: Accept Invitation (Existing User)
|
|
|
|
$invite = User_Model::where('invite_code', $code)
|
|
->whereNull('invite_accepted_at')
|
|
->where('invite_expires_at', '>', now())
|
|
->firstOrFail();
|
|
|
|
$login_user = RsxAuth::user();
|
|
|
|
if ($login_user->email !== $invite->email) {
|
|
return error('Email mismatch. This invite was sent to a different email.');
|
|
}
|
|
|
|
$invite->login_user_id = $login_user->id;
|
|
$invite->invite_accepted_at = now();
|
|
$invite->save();
|
|
|
|
Session::set_site_id($invite->site_id);
|
|
return redirect('/dashboard');
|
|
|
|
Example 5: Site Selection After Login
|
|
|
|
$login_user = RsxAuth::user();
|
|
|
|
$sites = User_Model::where('login_user_id', $login_user->id)
|
|
->whereNotNull('invite_accepted_at')
|
|
->where('invite_expires_at', '>', now())
|
|
->with('site')
|
|
->get();
|
|
|
|
if ($sites->count() === 0) {
|
|
RsxAuth::logout();
|
|
return error('No site access');
|
|
}
|
|
|
|
if ($sites->count() === 1) {
|
|
Session::set_site_id($sites[0]->site_id);
|
|
return redirect('/dashboard');
|
|
}
|
|
|
|
// Multiple sites - show picker
|
|
return view('auth/site-picker', ['sites' => $sites]);
|
|
|
|
Example 6: Protect Route with Custom Permission
|
|
|
|
In rsx/permission.php:
|
|
|
|
public static function is_owner(): bool|Response
|
|
{
|
|
if (!RsxAuth::check()) {
|
|
return false;
|
|
}
|
|
|
|
$membership = User_Model::where('login_user_id', RsxAuth::id())
|
|
->where('site_id', Session::get_site_id())
|
|
->first();
|
|
|
|
return $membership && $membership->role_id === 1;
|
|
}
|
|
|
|
In controller:
|
|
|
|
#[Route('/admin/delete-site')]
|
|
#[Auth('Permission::is_owner()')]
|
|
public static function delete_site(Request $request, array $params = [])
|
|
{
|
|
// Only site owners can access this
|
|
}
|
|
|
|
FUTURE PLANNED FEATURES
|
|
|
|
The following features are documented here for completeness but not yet
|
|
implemented. They represent the roadmap for authentication enhancements.
|
|
|
|
Enterprise Domain Locking:
|
|
Allow enterprise admins to "claim" email domains and control how
|
|
corporate email addresses are used with the platform.
|
|
|
|
Use Cases:
|
|
- Corporate IT controls where company email addresses are used
|
|
- Prevents employees from accepting invitations to external sites
|
|
using corporate email
|
|
- Centralized governance of corporate identity usage
|
|
- Audit trail of all sites using company domain
|
|
|
|
Domain Claiming:
|
|
- Enterprise admin claims domain (e.g., @acmecorp.com)
|
|
- Proves ownership via DNS TXT record or email verification
|
|
- Sets policy for that domain across entire platform
|
|
|
|
Policy Options:
|
|
1. Block External Sites
|
|
- Employees cannot accept invitations to other sites
|
|
- All @acmecorp.com addresses restricted to company's sites only
|
|
- Invitations to external sites automatically rejected
|
|
|
|
2. Require Approval
|
|
- External invitations go to approval queue
|
|
- Enterprise admin reviews and approves/rejects
|
|
- Approved invitations proceed normally
|
|
|
|
3. Allow But Notify
|
|
- Employees can accept any invitation
|
|
- Enterprise admin receives notification
|
|
- Provides audit trail without blocking access
|
|
|
|
Implementation Considerations:
|
|
- Similar to Slack's "Workspace Claiming" feature
|
|
- Similar to Microsoft's "Tenant Restrictions"
|
|
- Standard enterprise feature in B2B SaaS
|
|
- Balances employee flexibility with corporate control
|
|
|
|
Future Enhancement:
|
|
- SSO integration (SAML/OAuth)
|
|
- Automatic provisioning/deprovisioning via SCIM
|
|
- Group-based access control
|
|
- Conditional access policies
|
|
|
|
User Quota Enforcement:
|
|
- Track user count per site (sites.user_count)
|
|
- Prevent invitations when at limit
|
|
- Different quota tiers per plan/subscription
|
|
- Grace period for over-quota sites
|
|
- Admin override for quota enforcement
|
|
|
|
Email System Integration:
|
|
- Send invitation emails with acceptance link
|
|
- Send acceptance confirmation to admin
|
|
- Send "User joined site" notification
|
|
- Admin notification for email mismatch requests
|
|
- Resend invitation functionality
|
|
- Email template customization per site
|
|
|
|
Invitation Management UI:
|
|
- Resend invitation (generates new code)
|
|
- Cancel/revoke pending invitations
|
|
- View invitation status and history
|
|
- Bulk invitation import (CSV)
|
|
- Invitation acceptance tracking
|
|
|
|
Subdomain-Based Tenant Isolation:
|
|
- Traditional login flow (have account or don't)
|
|
- No site picker (determined by subdomain)
|
|
- Site-specific branding and theming
|
|
- Custom domains per tenant
|
|
- DNS-based site enforcement
|
|
|
|
Advanced Permissions:
|
|
- Granular permission system beyond roles
|
|
- Custom permissions per site
|
|
- Permission inheritance and grouping
|
|
- Role-based access control (RBAC) framework
|
|
- Permission caching for performance
|
|
|
|
Account Management:
|
|
- Password reset flow
|
|
- Email change verification
|
|
- Two-factor authentication (2FA)
|
|
- Account recovery options
|
|
- Login history and security audit log
|
|
|
|
Session Management:
|
|
- Active session listing
|
|
- Force logout from all devices
|
|
- Session activity monitoring
|
|
- Concurrent session limits
|
|
- IP-based session validation
|
|
|
|
SEE ALSO
|
|
session - RSX session management API reference
|
|
model - Model system with relationships
|
|
routing - Type-safe URL generation and route patterns
|
|
permission - Custom permission implementation
|