From 95ced77a57a2e96ea2934833a3e3f2ed2eeda947 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Nov 2025 20:33:54 +0000 Subject: [PATCH 1/6] Add comprehensive Docusaurus documentation and update README - Created comprehensive documentation in /docs folder with 12 markdown files - Added sidebar_position to all docs following the order in README.md - Fixed inaccurate code examples in README.md to match current implementation - Updated README with links to all documentation sections - Fixed example.php to use correct API (UsersAnyDataset constructor, SessionContext) - Organized docs with proper Docusaurus frontmatter and features - Added detailed guides for: * Getting started and installation * User management (CRUD operations) * Authentication methods (session and JWT) * Session context and storage options * User properties (custom key-value data) * Database storage and schema customization * Password validation and generation * JWT token authentication * Custom fields and extending UserModel * Mappers and entity processors * Complete working examples - Removed duplicate and outdated information from README - Added feature list and improved organization - All code examples now reflect actual API usage --- README.md | 419 ++++++++++++++--------------- docs/authentication.md | 150 +++++++++++ docs/custom-fields.md | 375 ++++++++++++++++++++++++++ docs/database-storage.md | 256 ++++++++++++++++++ docs/examples.md | 520 ++++++++++++++++++++++++++++++++++++ docs/getting-started.md | 58 ++++ docs/installation.md | 51 ++++ docs/jwt-tokens.md | 382 ++++++++++++++++++++++++++ docs/mappers.md | 433 ++++++++++++++++++++++++++++++ docs/password-validation.md | 288 ++++++++++++++++++++ docs/session-context.md | 197 ++++++++++++++ docs/user-management.md | 154 +++++++++++ docs/user-properties.md | 248 +++++++++++++++++ example.php | 43 ++- 14 files changed, 3347 insertions(+), 227 deletions(-) create mode 100644 docs/authentication.md create mode 100644 docs/custom-fields.md create mode 100644 docs/database-storage.md create mode 100644 docs/examples.md create mode 100644 docs/getting-started.md create mode 100644 docs/installation.md create mode 100644 docs/jwt-tokens.md create mode 100644 docs/mappers.md create mode 100644 docs/password-validation.md create mode 100644 docs/session-context.md create mode 100644 docs/user-management.md create mode 100644 docs/user-properties.md diff --git a/README.md b/README.md index a16b060..b973e33 100644 --- a/README.md +++ b/README.md @@ -6,297 +6,258 @@ [![GitHub license](https://img.shields.io/github/license/byjg/php-authuser.svg)](https://opensource.byjg.com/opensource/licensing.html) [![GitHub release](https://img.shields.io/github/release/byjg/php-authuser.svg)](https://github.com/byjg/php-authuser/releases/) -A simple and customizable class for enable user authentication inside your application. It is available on XML files, Relational Databases. +A simple and customizable library for user authentication in PHP applications. It supports multiple storage backends including databases and XML files. -The main purpose is just to handle all complexity of validate a user, add properties and create access token abstracting the database layer. -This class can persist into session (or file, memcache, etc) the user data between requests. +The main purpose is to handle all complexity of user validation, authentication, properties management, and access tokens, abstracting the database layer. +This class can persist user data into session (or file, memcache, etc.) between requests. -## Creating a Users handling class +## Documentation -Using the FileSystem (XML) as the user storage: +- [Getting Started](docs/getting-started.md) +- [Installation](docs/installation.md) +- [User Management](docs/user-management.md) +- [Authentication](docs/authentication.md) +- [Session Context](docs/session-context.md) +- [User Properties](docs/user-properties.md) +- [Database Storage](docs/database-storage.md) +- [Password Validation](docs/password-validation.md) +- [JWT Tokens](docs/jwt-tokens.md) +- [Custom Fields](docs/custom-fields.md) +- [Mappers](docs/mappers.md) +- [Examples](docs/examples.md) -```php -isAuthenticated()) { - - // Get the userId of the authenticated users - $userId = $sessionContext->userInfo(); +// Create or load AnyDataset +$anyDataset = new AnyDataset('/tmp/users.xml'); - // Get the user and your name - $user = $users->getById($userId); - echo "Hello: " . $user->getName(); -} +// Initialize user management +$users = new UsersAnyDataset($anyDataset); ``` -## Saving extra info into the user session +*Note*: See the [AnyDataset DB project](https://github.com/byjg/anydataset-db) for available databases and connection strings. -You can save data in the session data exists only during the user is logged in. Once the user logged off the -data stored with the user session will be released. +## Basic Usage -Store the data for the current user session: +### Creating and Authenticating Users ```php setSessionData('key', 'value'); -``` +use ByJG\Authenticate\SessionContext; +use ByJG\Cache\Factory; -Getting the data from the current user session: +// Add a new user +$user = $users->addUser('John Doe', 'johndoe', 'john@example.com', 'SecurePass123'); -```php -getSessionData('key'); -``` +// Validate user credentials +$authenticatedUser = $users->isValidUser('johndoe', 'SecurePass123'); -Note: If the user is not logged an error will be throw +if ($authenticatedUser !== null) { + // Create session context + $sessionContext = new SessionContext(Factory::createSessionPool()); -## Adding a custom property to the users + // Register the login + $sessionContext->registerLogin($authenticatedUser->getUserid()); -```php -getById($userId); -$user->setField('somefield', 'somevalue'); -$users->save(); + echo "Welcome, " . $authenticatedUser->getName(); +} ``` -## Logout from a session +### Check if User is Authenticated ```php registerLogout(); -``` +$sessionContext = new SessionContext(Factory::createSessionPool()); -## Important note about SessionContext +// Check if the user is authenticated +if ($sessionContext->isAuthenticated()) { + // Get the userId of the authenticated user + $userId = $sessionContext->userInfo(); -`SessionContext` object will store the info about the current context. -As SessionContext uses CachePool interface defined in PSR-6 you can set any storage -to save your session context. + // Get the user and display name + $user = $users->getById($userId); + echo "Hello: " . $user->getName(); +} else { + echo "Please log in"; +} +``` -In our examples we are using a regular PHP Session for store the user context -(`Factory::createSessionPool()`). But if you are using another store like MemCached -you have to define a UNIQUE prefix for that session. Note if TWO users have the same -prefix you probably have an unexpected result for the SessionContext. +## Managing Session Data -Example for memcached: +You can store temporary data in the user session that exists only while the user is logged in. Once the user logs out, the data is automatically released. + +### Store Session Data ```php setSessionData('shopping_cart', [ + 'item1' => 'Product A', + 'item2' => 'Product B' +]); -## Architecture - -```text - ┌───────────────────┐ - │ SessionContext │ - └───────────────────┘ - │ -┌────────────────────────┐ ┌────────────────────────┐ -│ UserDefinition │─ ─ ┐ │ ─ ─ ┤ UserModel │ -└────────────────────────┘ ┌───────────────────┐ │ └────────────────────────┘ -┌────────────────────────┐ └────│ UsersInterface │────┐ ┌────────────────────────┐ -│ UserPropertyDefinition │─ ─ ┘ └───────────────────┘ ─ ─ ┤ UserPropertyModel │ -└────────────────────────┘ ▲ └────────────────────────┘ - │ - ┌────────────────────────┼─────────────────────────┐ - │ │ │ - │ │ │ - │ │ │ - ┌───────────────────┐ ┌───────────────────┐ ┌────────────────────┐ - │ UsersAnyDataset │ │ UsersDBDataset │ │ xxxxxxxxxxxxxxxxxx │ - └───────────────────┘ └───────────────────┘ └────────────────────┘ +$sessionContext->setSessionData('last_page', '/products'); ``` -- UserInterface contain the basic interface for the concrete implementation -- UsersDBDataset is a concrete implementation to retrieve/save user in a Database -- UserAnyDataset is a concrete implementation to retrieve/save user in a Xml file -- UserModel is the basic model get/set for the user -- UserPropertyModel is the basic model get/set for extra user property -- UserDefinition will map the model to the database - -### Database +### Retrieve Session Data -The default structure adopted for store the user data in the database through the -UsersDBDataset class is the follow: +```php +getSessionData('shopping_cart'); +$lastPage = $sessionContext->getSessionData('last_page'); ``` -Using the database structure above you can create the UsersDBDatase as follow: +:::note +A `NotAuthenticatedException` will be thrown if the user is not authenticated when accessing session data. +::: -```php - 'fieldname of userid', - UserDefinition::FIELD_NAME => 'fieldname of name', - UserDefinition::FIELD_EMAIL => 'fieldname of email', - UserDefinition::FIELD_USERNAME => 'fieldname of username', - UserDefinition::FIELD_PASSWORD => 'fieldname of password', - UserDefinition::FIELD_CREATED => 'fieldname of created', - UserDefinition::FIELD_ADMIN => 'fieldname of admin' - ] -); +// Add a property to a user +$users->addProperty($userId, 'phone', '555-1234'); +$users->addProperty($userId, 'department', 'Engineering'); + +// Users can have multiple values for the same property +$users->addProperty($userId, 'role', 'developer'); +$users->addProperty($userId, 'role', 'manager'); ``` -### Adding custom modifiers for read and update +### Using UserModel ```php the current value to be updated -// $instance -> The array with all other fields; -$userDefinition->defineClosureForUpdate(UserDefinition::FIELD_PASSWORD, function ($value, $instance) { - return strtoupper(sha1($value)); -}); - -// Defines a custom function to be applied After the field UserDefinition::FIELD_CREATED is read but before -// the user get the result -// $value --> the current value retrieved from database -// $instance -> The array with all other fields; -$userDefinition->defineClosureForSelect(UserDefinition::FIELD_CREATED, function ($value, $instance) { - return date('Y', $value); -}); - -// If you want make the field READONLY just do it: -$userDefinition->markPropertyAsReadOnly(UserDefinition::FIELD_CREATED); -``` - -## Extending UserModel +$user = $users->getById($userId); -It is possible extending the UserModel table, since you create a new class extending from UserModel to add the new fields. +// Set a property (update or create) +$user->set('phone', '555-1234'); -For example, imagine your table has one field called "otherfield". +// Save changes +$users->save($user); +``` -You'll have to extend like this: +## Logout ```php setOtherfield($field); - } - - public function getOtherfield() - { - return $this->otherfield; - } - - public function setOtherfield($otherfield) - { - $this->otherfield = $otherfield; - } -} +$sessionContext->registerLogout(); ``` -After that you can use your new definition: +## JWT Token Authentication + +For stateless API authentication, you can use JWT tokens: ```php createAuthToken( + 'johndoe', // Login + 'SecurePass123', // Password + $jwtWrapper, + 3600, // Expires in 1 hour (seconds) + [], // Additional user info to save + ['role' => 'admin'] // Additional token data ); + +// Validate token +$result = $users->isValidToken('johndoe', $jwtWrapper, $token); +if ($result !== null) { + $user = $result['user']; + $tokenData = $result['data']; +} ``` -## Install +See [JWT Tokens](docs/jwt-tokens.md) for detailed information. -Just type: +## Database Schema -```bash -composer require "byjg/authuser" +The default database schema uses two tables: + +```sql +CREATE TABLE users ( + userid INTEGER AUTO_INCREMENT NOT NULL, + name VARCHAR(50), + email VARCHAR(120), + username VARCHAR(15) NOT NULL, + password CHAR(40) NOT NULL, + created DATETIME, + admin ENUM('Y','N'), + CONSTRAINT pk_users PRIMARY KEY (userid) +) ENGINE=InnoDB; + +CREATE TABLE users_property ( + customid INTEGER AUTO_INCREMENT NOT NULL, + name VARCHAR(20), + value VARCHAR(100), + userid INTEGER NOT NULL, + CONSTRAINT pk_custom PRIMARY KEY (customid), + CONSTRAINT fk_custom_user FOREIGN KEY (userid) REFERENCES users (userid) +) ENGINE=InnoDB; ``` +You can customize table and column names using `UserDefinition` and `UserPropertiesDefinition`. See [Database Storage](docs/database-storage.md) for details. + +## Features + +- **Complete User Management** - Create, read, update, and delete users +- **Flexible Authentication** - Username/email + password or JWT tokens +- **Session Management** - PSR-6 compatible cache storage +- **User Properties** - Store custom key-value data per user +- **Password Validation** - Built-in password strength requirements +- **Multiple Storage Backends** - Database (MySQL, PostgreSQL, SQLite, etc.) or XML files +- **Customizable Schema** - Map to existing database tables +- **Field Mappers** - Transform data during read/write operations +- **Extensible User Model** - Add custom fields easily + ## Running Tests Because this project uses PHP Session you need to run the unit test the following manner: @@ -305,15 +266,41 @@ Because this project uses PHP Session you need to run the unit test the followin ./vendor/bin/phpunit --stderr ``` +## Architecture + +```text + ┌───────────────────┐ + │ SessionContext │ + └───────────────────┘ + │ +┌────────────────────────┐ ┌────────────────────────┐ +│ UserDefinition │─ ─ ┐ │ ─ ─ ┤ UserModel │ +└────────────────────────┘ ┌───────────────────┐ │ └────────────────────────┘ +┌────────────────────────┐ └────│ UsersInterface │────┐ ┌────────────────────────┐ +│ UserPropertyDefinition │─ ─ ┘ └───────────────────┘ ─ ─ ┤ UserPropertyModel │ +└────────────────────────┘ ▲ └────────────────────────┘ + │ + ┌────────────────────────┼─────────────────────────┐ + │ │ │ + │ │ │ + │ │ │ + ┌───────────────────┐ ┌───────────────────┐ ┌────────────────────┐ + │ UsersAnyDataset │ │ UsersDBDataset │ │ Custom Impl. │ + └───────────────────┘ └───────────────────┘ └────────────────────┘ +``` + ## Dependencies -```mermaid -flowchart TD +```mermaid +flowchart TD byjg/authuser --> byjg/micro-orm byjg/authuser --> byjg/cache-engine - byjg/authuser --> byjg/jwt-wrapper + byjg/authuser --> byjg/jwt-wrapper ``` +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. ---- [Open source ByJG](http://opensource.byjg.com) diff --git a/docs/authentication.md b/docs/authentication.md new file mode 100644 index 0000000..b18cc31 --- /dev/null +++ b/docs/authentication.md @@ -0,0 +1,150 @@ +--- +sidebar_position: 4 +title: Authentication +--- + +# Authentication + +## Validating User Credentials + +Use the `isValidUser()` method to validate a username/email and password combination: + +```php +isValidUser('johndoe', 'SecurePass123'); + +if ($user !== null) { + echo "Authentication successful!"; + echo "User ID: " . $user->getUserid(); +} else { + echo "Invalid credentials"; +} +``` + +:::tip Login Field +The `isValidUser()` method uses the login field defined in your `UserDefinition`. This can be either the email or username field. +::: + +## Password Hashing + +By default, passwords are automatically hashed using SHA-1 when saved. The library uses the `PasswordSha1Mapper` for this purpose. + +```php +setPassword('plaintext password'); +$users->save($user); + +// The password is stored as SHA-1 hash in the database +``` + +:::warning SHA-1 Deprecation +SHA-1 is used for backward compatibility. For new projects, consider implementing a custom password hasher using bcrypt or Argon2. See [Custom Mappers](mappers.md) for details. +::: + +## Workflow + +### Basic Authentication Flow + +```php +isValidUser('johndoe', 'SecurePass123'); + +if ($user !== null) { + // 2. Create session context + $sessionContext = new SessionContext(Factory::createSessionPool()); + + // 3. Register login + $sessionContext->registerLogin($user->getUserid()); + + // 4. User is now authenticated + echo "Welcome, " . $user->getName(); +} +``` + +### Checking Authentication Status + +```php +isAuthenticated()) { + $userId = $sessionContext->userInfo(); + $user = $users->getById($userId); + echo "Hello, " . $user->getName(); +} else { + echo "Please log in"; +} +``` + +### Logging Out + +```php +registerLogout(); +``` + +## JWT Token Authentication + +For stateless authentication, you can use JWT tokens: + +```php +createAuthToken( + 'johndoe', // Login + 'SecurePass123', // Password + $jwtWrapper, + 3600, // Expires in 1 hour (seconds) + [], // Additional user info to save + ['role' => 'admin'] // Additional token data +); + +if ($token !== null) { + echo "Token: " . $token; +} +``` + +### Validating JWT Tokens + +```php +isValidToken('johndoe', $jwtWrapper, $token); + +if ($result !== null) { + $user = $result['user']; + $tokenData = $result['data']; + + echo "User: " . $user->getName(); + echo "Role: " . $tokenData['role']; +} +``` + +:::info Token Storage +When a JWT token is created, a hash of the token is stored in the user's properties as `TOKEN_HASH`. This ensures tokens can be invalidated if needed. +::: + +## Security Best Practices + +1. **Always use HTTPS** in production to prevent credential theft +2. **Implement rate limiting** to prevent brute force attacks +3. **Use strong passwords** - see [Password Validation](password-validation.md) +4. **Set appropriate session timeouts** +5. **Validate and sanitize** all user inputs + +## Next Steps + +- [Session Context](session-context.md) - Manage user sessions +- [JWT Tokens](jwt-tokens.md) - Deep dive into JWT authentication +- [Password Validation](password-validation.md) - Enforce password policies diff --git a/docs/custom-fields.md b/docs/custom-fields.md new file mode 100644 index 0000000..04d2bc5 --- /dev/null +++ b/docs/custom-fields.md @@ -0,0 +1,375 @@ +--- +sidebar_position: 10 +title: Custom Fields +--- + +# Custom Fields + +You can extend the `UserModel` to add custom fields that match your database schema. + +## Extending UserModel + +### Creating a Custom User Model + +```php +phone = $phone; + $this->department = $department; + } + + public function getPhone(): ?string + { + return $this->phone; + } + + public function setPhone(?string $phone): void + { + $this->phone = $phone; + } + + public function getDepartment(): ?string + { + return $this->department; + } + + public function setDepartment(?string $department): void + { + $this->department = $department; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(?string $title): void + { + $this->title = $title; + } + + public function getProfilePicture(): ?string + { + return $this->profilePicture; + } + + public function setProfilePicture(?string $profilePicture): void + { + $this->profilePicture = $profilePicture; + } +} +``` + +## Database Schema + +Add the custom fields to your users table: + +```sql +CREATE TABLE users +( + userid INTEGER AUTO_INCREMENT NOT NULL, + name VARCHAR(50), + email VARCHAR(120), + username VARCHAR(15) NOT NULL, + password CHAR(40) NOT NULL, + created DATETIME, + admin ENUM('Y','N'), + -- Custom fields + phone VARCHAR(20), + department VARCHAR(50), + title VARCHAR(50), + profile_picture VARCHAR(255), + + CONSTRAINT pk_users PRIMARY KEY (userid) +) ENGINE=InnoDB; +``` + +## Configuring UserDefinition + +Map the custom fields in your `UserDefinition`: + +```php + 'userid', + UserDefinition::FIELD_NAME => 'name', + UserDefinition::FIELD_EMAIL => 'email', + UserDefinition::FIELD_USERNAME => 'username', + UserDefinition::FIELD_PASSWORD => 'password', + UserDefinition::FIELD_CREATED => 'created', + UserDefinition::FIELD_ADMIN => 'admin', + // Custom fields + 'phone' => 'phone', + 'department' => 'department', + 'title' => 'title', + 'profilePicture' => 'profile_picture' + ] +); +``` + +## Using the Custom Model + +### Creating Users + +```php +setName('John Doe'); +$user->setEmail('john@example.com'); +$user->setUsername('johndoe'); +$user->setPassword('SecurePass123'); +$user->setPhone('+1-555-1234'); +$user->setDepartment('Engineering'); +$user->setTitle('Senior Developer'); + +$users->save($user); +``` + +### Retrieving Users + +```php +getById($userId); + +// Access custom fields +echo $user->getName(); +echo $user->getPhone(); +echo $user->getDepartment(); +echo $user->getTitle(); +``` + +### Updating Custom Fields + +```php +getById($userId); +$user->setDepartment('Sales'); +$user->setTitle('Sales Manager'); +$users->save($user); +``` + +## Read-Only Fields + +You can mark fields as read-only to prevent updates: + +```php +markPropertyAsReadOnly(UserDefinition::FIELD_CREATED); + +// Make custom field read-only +$userDefinition->markPropertyAsReadOnly('phone'); +``` + +Read-only fields: +- Can be set during creation +- Cannot be updated after creation +- Are ignored during updates + +## Auto-Generated Fields + +### Auto-Increment IDs + +For auto-increment IDs, the database handles generation automatically. No configuration needed. + +### UUID Fields + +For UUID primary keys: + +```php +defineGenerateKey(UserIdGeneratorMapper::class); +``` + +### Custom ID Generation + +Create a custom mapper for custom ID generation: + +```php +defineGenerateKey(CustomIdMapper::class); +``` + +## Field Transformation + +You can transform fields during read/write operations using mappers. See [Mappers](mappers.md) for details. + +## Complex Data Types + +### JSON Fields + +For storing JSON data in custom fields: + +```php +defineMapperForUpdate('metadata', JsonMapper::class); +$userDefinition->defineMapperForSelect('metadata', JsonDecodeMapper::class); +``` + +### Date/Time Fields + +```php +format('Y-m-d H:i:s'); + } + return $value; + } +} + +$userDefinition->defineMapperForUpdate('created', DateTimeMapper::class); +``` + +## Complete Example + +```php + 'userid', + UserDefinition::FIELD_NAME => 'name', + UserDefinition::FIELD_EMAIL => 'email', + UserDefinition::FIELD_USERNAME => 'username', + UserDefinition::FIELD_PASSWORD => 'password', + UserDefinition::FIELD_CREATED => 'created', + UserDefinition::FIELD_ADMIN => 'admin', + 'phone' => 'phone', + 'department' => 'department', + 'title' => 'title', + 'profilePicture' => 'profile_picture' + ] +); + +// Make created field read-only +$userDefinition->markPropertyAsReadOnly(UserDefinition::FIELD_CREATED); + +// Initialize user management +$users = new UsersDBDataset($dbDriver, $userDefinition); + +// Create a user +$user = new CustomUserModel(); +$user->setName('Jane Smith'); +$user->setEmail('jane@example.com'); +$user->setUsername('janesmith'); +$user->setPassword('SecurePass123'); +$user->setPhone('+1-555-5678'); +$user->setDepartment('Marketing'); +$user->setTitle('Marketing Director'); + +$savedUser = $users->save($user); + +// Retrieve and update +$user = $users->getById($savedUser->getUserid()); +$user->setTitle('VP of Marketing'); +$users->save($user); +``` + +## When to Use Custom Fields vs Properties + +| Use Custom Fields When | Use Properties When | +|------------------------|---------------------| +| Field is used frequently | Field is rarely used | +| Field is searched/filtered | Field is key-value metadata | +| Field is fixed schema | Field is dynamic/flexible | +| Better performance needed | Schema flexibility needed | +| Field is required | Field is optional | + +## Next Steps + +- [Mappers](mappers.md) - Custom field transformations +- [Database Storage](database-storage.md) - Schema configuration +- [User Properties](user-properties.md) - Flexible metadata storage diff --git a/docs/database-storage.md b/docs/database-storage.md new file mode 100644 index 0000000..8e9c872 --- /dev/null +++ b/docs/database-storage.md @@ -0,0 +1,256 @@ +--- +sidebar_position: 7 +title: Database Storage +--- + +# Database Storage + +The library supports storing users in relational databases through the `UsersDBDataset` class. + +## Database Setup + +### Default Schema + +The default database structure uses two tables: + +```sql +CREATE TABLE users +( + userid INTEGER AUTO_INCREMENT NOT NULL, + name VARCHAR(50), + email VARCHAR(120), + username VARCHAR(15) NOT NULL, + password CHAR(40) NOT NULL, + created DATETIME, + admin ENUM('Y','N'), + + CONSTRAINT pk_users PRIMARY KEY (userid) +) ENGINE=InnoDB; + +CREATE TABLE users_property +( + customid INTEGER AUTO_INCREMENT NOT NULL, + name VARCHAR(20), + value VARCHAR(100), + userid INTEGER NOT NULL, + + CONSTRAINT pk_custom PRIMARY KEY (customid), + CONSTRAINT fk_custom_user FOREIGN KEY (userid) REFERENCES users (userid) +) ENGINE=InnoDB; +``` + +## Basic Usage + +### Using Default Configuration + +```php + 'user_id', + UserDefinition::FIELD_NAME => 'full_name', + UserDefinition::FIELD_EMAIL => 'email_address', + UserDefinition::FIELD_USERNAME => 'user_name', + UserDefinition::FIELD_PASSWORD => 'password_hash', + UserDefinition::FIELD_CREATED => 'date_created', + UserDefinition::FIELD_ADMIN => 'is_admin' + ] +); + +$users = new UsersDBDataset($dbDriver, $userDefinition); +``` + +### Custom Properties Table + +```php + 'id', + UserDefinition::FIELD_NAME => 'fullname', + UserDefinition::FIELD_EMAIL => 'email', + UserDefinition::FIELD_USERNAME => 'username', + UserDefinition::FIELD_PASSWORD => 'pwd', + UserDefinition::FIELD_CREATED => 'created_at', + UserDefinition::FIELD_ADMIN => 'is_admin' + ] +); + +// Custom properties definition +$propertiesDefinition = new UserPropertiesDefinition( + 'app_user_meta', + 'id', + 'meta_key', + 'meta_value', + 'user_id' +); + +// Initialize +$users = new UsersDBDataset($dbDriver, $userDefinition, $propertiesDefinition); + +// Use it +$user = $users->addUser('John Doe', 'johndoe', 'john@example.com', 'password123'); +``` + +## XML/File Storage + +For simple applications or development, you can use XML file storage: + +```php +addUser('John Doe', 'johndoe', 'john@example.com', 'password123'); +``` + +:::warning Production Use +XML file storage is suitable for development and small applications. For production applications with multiple users, use database storage. +::: + +## Architecture + +```text + ┌───────────────────┐ + │ SessionContext │ + └───────────────────┘ + │ +┌────────────────────────┐ ┌────────────────────────┐ +│ UserDefinition │─ ─ ┐ │ ─ ─ ┤ UserModel │ +└────────────────────────┘ ┌───────────────────┐ │ └────────────────────────┘ +┌────────────────────────┐ └────│ UsersInterface │────┐ ┌────────────────────────┐ +│ UserPropertyDefinition │─ ─ ┘ └───────────────────┘ ─ ─ ┤ UserPropertyModel │ +└────────────────────────┘ ▲ └────────────────────────┘ + │ + ┌────────────────────────┼─────────────────────────┐ + │ │ │ + │ │ │ + │ │ │ + ┌───────────────────┐ ┌───────────────────┐ ┌────────────────────┐ + │ UsersAnyDataset │ │ UsersDBDataset │ │ Custom Impl. │ + └───────────────────┘ └───────────────────┘ └────────────────────┘ +``` + +- **UserInterface**: Base interface for all implementations +- **UsersDBDataset**: Database implementation +- **UsersAnyDataset**: XML file implementation +- **UserModel**: The user data model +- **UserPropertyModel**: The user property data model +- **UserDefinition**: Maps model to database schema +- **UserPropertiesDefinition**: Maps properties to database schema + +## Next Steps + +- [User Management](user-management.md) - Managing users +- [Custom Fields](custom-fields.md) - Extending UserModel +- [Mappers](mappers.md) - Custom field transformations diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 0000000..deba131 --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,520 @@ +--- +sidebar_position: 12 +title: Complete Examples +--- + +# Complete Examples + +This page contains complete, working examples for common use cases. + +## Simple Web Application + +### Setup + +```php +isValidUser($username, $password); + + if ($user !== null) { + $sessionContext->registerLogin($user->getUserid()); + $sessionContext->setSessionData('login_time', time()); + + header('Location: dashboard.php'); + exit; + } else { + $error = 'Invalid username or password'; + } + } catch (Exception $e) { + $error = 'An error occurred: ' . $e->getMessage(); + } +} +?> + + + + Login + + +

Login

+ + +
+ + +
+
+ + +
+
+ + +
+ +
+ +

Create an account

+ + +``` + +### Registration Page + +```php + 8, + PasswordDefinition::REQUIRE_UPPERCASE => 1, + PasswordDefinition::REQUIRE_LOWERCASE => 1, + PasswordDefinition::REQUIRE_NUMBERS => 1, + ]); + + $result = $passwordDef->matchPassword($password); + if ($result !== PasswordDefinition::SUCCESS) { + throw new Exception('Password does not meet requirements'); + } + + // Create user + $user = $users->addUser($name, $username, $email, $password); + + // Auto-login + $sessionContext->registerLogin($user->getUserid()); + + header('Location: dashboard.php'); + exit; + + } catch (UserExistsException $e) { + $error = 'Username or email already exists'; + } catch (Exception $e) { + $error = $e->getMessage(); + } +} +?> + + + + Register + + +

Create Account

+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + Minimum 8 characters, at least 1 uppercase, 1 lowercase, and 1 number +
+
+ + +
+ +
+ +

Already have an account?

+ + +``` + +### Protected Dashboard + +```php +isAuthenticated()) { + header('Location: login.php'); + exit; +} + +// Get current user +$userId = $sessionContext->userInfo(); +$user = $users->getById($userId); +$loginTime = $sessionContext->getSessionData('login_time'); +?> + + + + Dashboard + + +

Welcome, getName()) ?>

+ +

Email: getEmail()) ?>

+

Logged in at:

+ + isAdmin($userId)): ?> +

You are an administrator

+

Admin Panel

+ + +

Edit Profile

+

Logout

+ + +``` + +### Logout + +```php +registerLogout(); +session_destroy(); + +header('Location: login.php'); +exit; +``` + +## REST API with JWT + +### API Configuration + +```php + 'Method not allowed'], 405); +} + +$input = json_decode(file_get_contents('php://input'), true); +$username = $input['username'] ?? ''; +$password = $input['password'] ?? ''; + +try { + $token = $users->createAuthToken( + $username, + $password, + $jwtWrapper, + 3600, // 1 hour + [ + 'last_login' => date('Y-m-d H:i:s'), + 'last_ip' => $_SERVER['REMOTE_ADDR'] + ], + [ + 'ip' => $_SERVER['REMOTE_ADDR'] + ] + ); + + if ($token === null) { + jsonResponse(['error' => 'Invalid credentials'], 401); + } + + jsonResponse([ + 'success' => true, + 'token' => $token, + 'expires_in' => 3600 + ]); + +} catch (Exception $e) { + jsonResponse(['error' => $e->getMessage()], 500); +} +``` + +### Protected Endpoint + +```php + 'No token provided'], 401); +} + +$token = $matches[1]; + +try { + // Decode token to get username + $jwtData = $jwtWrapper->extractData($token); + $username = $jwtData->data['login'] ?? null; + + if (!$username) { + jsonResponse(['error' => 'Invalid token'], 401); + } + + // Validate token + $result = $users->isValidToken($username, $jwtWrapper, $token); + + if ($result === null) { + jsonResponse(['error' => 'Token validation failed'], 401); + } + + $user = $result['user']; + + // Handle request + if ($_SERVER['REQUEST_METHOD'] === 'GET') { + // Get user info + jsonResponse([ + 'id' => $user->getUserid(), + 'name' => $user->getName(), + 'email' => $user->getEmail(), + 'username' => $user->getUsername(), + 'admin' => $users->isAdmin($user->getUserid()) + ]); + } elseif ($_SERVER['REQUEST_METHOD'] === 'PUT') { + // Update user info + $input = json_decode(file_get_contents('php://input'), true); + + if (isset($input['name'])) { + $user->setName($input['name']); + } + if (isset($input['email'])) { + $user->setEmail($input['email']); + } + + $users->save($user); + + jsonResponse(['success' => true, 'message' => 'User updated']); + } else { + jsonResponse(['error' => 'Method not allowed'], 405); + } + +} catch (Exception $e) { + jsonResponse(['error' => $e->getMessage()], 500); +} +``` + +## Multi-Tenant Application + +```php +addProperty($userId, 'organization', $orgId); + $users->addProperty($userId, "org_{$orgId}_role", $role); +} + +// Check if user has access to organization +function hasOrganizationAccess($users, $userId, $orgId) +{ + return $users->hasProperty($userId, 'organization', $orgId); +} + +// Get user's role in organization +function getOrganizationRole($users, $userId, $orgId) +{ + return $users->getProperty($userId, "org_{$orgId}_role"); +} + +// Get all users in organization +function getOrganizationUsers($users, $orgId) +{ + return $users->getUsersByProperty('organization', $orgId); +} + +// Usage +$userId = 1; +$orgId = 'org-123'; + +// Add user to organization +addUserToOrganization($users, $userId, $orgId, 'admin'); + +// Check access +if (hasOrganizationAccess($users, $userId, $orgId)) { + $role = getOrganizationRole($users, $userId, $orgId); + echo "User has access as: $role\n"; + + // Get all members + $members = getOrganizationUsers($users, $orgId); + foreach ($members as $member) { + echo "- " . $member->getName() . "\n"; + } +} +``` + +## Permission System + +```php +users = $users; + } + + public function grantPermission($userId, $resource, $action) + { + $permission = "$resource:$action"; + $this->users->addProperty($userId, 'permission', $permission); + } + + public function revokePermission($userId, $resource, $action) + { + $permission = "$resource:$action"; + $this->users->removeProperty($userId, 'permission', $permission); + } + + public function hasPermission($userId, $resource, $action) + { + $permission = "$resource:$action"; + return $this->users->hasProperty($userId, 'permission', $permission); + } + + public function getPermissions($userId) + { + $permissions = $this->users->getProperty($userId, 'permission'); + return is_array($permissions) ? $permissions : [$permissions]; + } +} + +// Usage +$permissionManager = new PermissionManager($users); + +// Grant permissions +$permissionManager->grantPermission($userId, 'posts', 'create'); +$permissionManager->grantPermission($userId, 'posts', 'edit'); +$permissionManager->grantPermission($userId, 'posts', 'delete'); +$permissionManager->grantPermission($userId, 'users', 'view'); + +// Check permissions +if ($permissionManager->hasPermission($userId, 'posts', 'delete')) { + echo "User can delete posts\n"; +} + +// Get all permissions +$permissions = $permissionManager->getPermissions($userId); +print_r($permissions); + +// Revoke permission +$permissionManager->revokePermission($userId, 'posts', 'delete'); +``` + +## Next Steps + +- [Getting Started](getting-started.md) - Basic concepts +- [User Management](user-management.md) - Managing users +- [Authentication](authentication.md) - Authentication methods +- [JWT Tokens](jwt-tokens.md) - Token-based authentication diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..aaa8bf6 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,58 @@ +--- +sidebar_position: 1 +title: Getting Started +--- + +# Getting Started + +Auth User PHP is a simple and customizable library for user authentication in PHP applications. It provides an abstraction layer for managing users, authentication, and user properties, supporting multiple storage backends including databases and XML files. + +## Key Features + +- **User Management**: Complete CRUD operations for users +- **Authentication**: Validate user credentials and manage sessions +- **User Properties**: Store and retrieve custom user properties +- **JWT Support**: Create and validate JWT tokens for stateless authentication +- **Password Validation**: Built-in password strength validation +- **Flexible Storage**: Support for databases (via AnyDataset) and XML files +- **Session Management**: PSR-6 compatible cache for session storage + +## Quick Example + +Here's a quick example of how to use the library: + +```php +addUser('John Doe', 'johndoe', 'john@example.com', 'SecurePass123'); + +// Validate user credentials +$user = $users->isValidUser('johndoe', 'SecurePass123'); + +if ($user !== null) { + // Create a session + $sessionContext = new SessionContext(Factory::createSessionPool()); + $sessionContext->registerLogin($user->getUserid()); + + echo "User authenticated successfully!"; +} +``` + +## Next Steps + +- [Installation](installation.md) - Install the library via Composer +- [User Management](user-management.md) - Learn how to manage users +- [Authentication](authentication.md) - Understand authentication methods +- [Session Context](session-context.md) - Manage user sessions diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..68852ae --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,51 @@ +--- +sidebar_position: 2 +title: Installation +--- + +# Installation + +## Requirements + +- PHP 8.1 or higher +- Composer + +## Install via Composer + +Install the library using Composer: + +```bash +composer require byjg/authuser +``` + +## Dependencies + +The library depends on the following packages: + +- `byjg/micro-orm` - For database operations +- `byjg/cache-engine` - For session management +- `byjg/jwt-wrapper` - For JWT token support + +These dependencies are automatically installed by Composer. + +:::info Dependency Graph +```mermaid +flowchart TD + byjg/authuser --> byjg/micro-orm + byjg/authuser --> byjg/cache-engine + byjg/authuser --> byjg/jwt-wrapper +``` +::: + +## Running Tests + +Because this project uses PHP Session, you need to run the unit tests with the `--stderr` flag: + +```bash +./vendor/bin/phpunit --stderr +``` + +## Next Steps + +- [Getting Started](getting-started.md) - Learn the basics +- [Database Storage](database-storage.md) - Set up database storage diff --git a/docs/jwt-tokens.md b/docs/jwt-tokens.md new file mode 100644 index 0000000..0993d28 --- /dev/null +++ b/docs/jwt-tokens.md @@ -0,0 +1,382 @@ +--- +sidebar_position: 9 +title: JWT Tokens +--- + +# JWT Tokens + +The library provides built-in support for JWT (JSON Web Token) authentication through integration with [byjg/jwt-wrapper](https://github.com/byjg/jwt-wrapper). + +## What is JWT? + +JWT (JSON Web Tokens) is a compact, URL-safe means of representing claims to be transferred between two parties. JWTs are commonly used for: + +- **Stateless authentication** - No server-side session storage needed +- **API authentication** - Perfect for REST APIs and microservices +- **Single Sign-On (SSO)** - Share authentication across domains +- **Mobile apps** - Efficient token-based authentication + +## Setup + +### Creating a JWT Wrapper + +```php +createAuthToken( + 'johndoe', // Login (username or email) + 'password123', // Password + $jwtWrapper, // JWT wrapper instance + 3600 // Expires in 1 hour (seconds) +); + +if ($token !== null) { + // Return token to client + echo json_encode(['token' => $token]); +} else { + // Authentication failed + http_response_code(401); + echo json_encode(['error' => 'Invalid credentials']); +} +``` + +### Token with Custom Data + +You can include additional data in the JWT payload: + +```php +createAuthToken( + 'johndoe', + 'password123', + $jwtWrapper, + 3600, + [], // Update user properties (optional) + [ // Additional token data + 'role' => 'admin', + 'permissions' => ['read', 'write'], + 'tenant_id' => '12345' + ] +); +``` + +### Update User Properties on Login + +```php +createAuthToken( + 'johndoe', + 'password123', + $jwtWrapper, + 3600, + [ // User properties to update + 'last_login' => date('Y-m-d H:i:s'), + 'login_count' => $loginCount + 1 + ], + [ // Token data + 'role' => 'admin' + ] +); +``` + +## Validating JWT Tokens + +### Token Validation + +```php +isValidToken('johndoe', $jwtWrapper, $token); + + if ($result !== null) { + $user = $result['user']; // UserModel instance + $tokenData = $result['data']; // Token payload data + + echo "Authenticated: " . $user->getName(); + echo "Role: " . $tokenData['role']; + } + +} catch (\ByJG\Authenticate\Exception\UserNotFoundException $e) { + echo "User not found"; +} catch (\ByJG\Authenticate\Exception\NotAuthenticatedException $e) { + echo "Token validation failed: " . $e->getMessage(); +} catch (\ByJG\JwtWrapper\JwtWrapperException $e) { + echo "JWT error: " . $e->getMessage(); +} +``` + +### Validation Checks + +The `isValidToken()` method performs the following checks: + +1. **User exists** - Verifies the user account exists +2. **Token hash matches** - Compares stored token hash +3. **JWT signature** - Validates the token signature +4. **Token expiration** - Checks if token has expired + +## Token Storage and Invalidation + +### How Tokens Are Stored + +When a token is created: + +```php +set('TOKEN_HASH', $tokenHash); +``` + +This allows you to invalidate tokens without maintaining a token blacklist. + +### Invalidating Tokens + +#### Logout (Invalidate Current Token) + +```php +removeProperty($userId, 'TOKEN_HASH'); +``` + +#### Force Re-authentication (Invalidate All Tokens) + +```php +createAuthToken($login, $password, $jwtWrapper, 3600); +``` + +## Complete API Example + +### Login Endpoint + +```php +createAuthToken( + $input['username'], + $input['password'], + $jwtWrapper, + 3600, // 1 hour expiration + [ + 'last_login' => date('Y-m-d H:i:s'), + 'last_ip' => $_SERVER['REMOTE_ADDR'] + ], + [ + 'ip' => $_SERVER['REMOTE_ADDR'], + 'user_agent' => $_SERVER['HTTP_USER_AGENT'] + ] + ); + + if ($token === null) { + throw new Exception('Authentication failed'); + } + + echo json_encode([ + 'success' => true, + 'token' => $token, + 'expires_in' => 3600 + ]); + +} catch (Exception $e) { + http_response_code(401); + echo json_encode([ + 'success' => false, + 'error' => $e->getMessage() + ]); +} +``` + +### Protected Endpoint + +```php + 'No token provided']); + exit; +} + +$token = $matches[1]; + +// Extract username from token (you need to decode it first) +try { + $jwtData = $jwtWrapper->extractData($token); + $username = $jwtData->data['login'] ?? null; + + if (!$username) { + throw new Exception('Invalid token structure'); + } + + // Validate token + $result = $users->isValidToken($username, $jwtWrapper, $token); + + if ($result === null) { + throw new Exception('Invalid token'); + } + + $user = $result['user']; + + // Process request + echo json_encode([ + 'success' => true, + 'user' => [ + 'id' => $user->getUserid(), + 'name' => $user->getName(), + 'email' => $user->getEmail() + ] + ]); + +} catch (Exception $e) { + http_response_code(401); + echo json_encode(['error' => $e->getMessage()]); +} +``` + +### Logout Endpoint + +```php +extractData($token); + $username = $jwtData->data['login'] ?? null; + + $user = $users->getByLoginField($username); + if ($user !== null) { + $users->removeProperty($user->getUserid(), 'TOKEN_HASH'); + } + + echo json_encode(['success' => true, 'message' => 'Logged out']); + + } catch (Exception $e) { + echo json_encode(['success' => false, 'error' => $e->getMessage()]); + } +} else { + http_response_code(400); + echo json_encode(['error' => 'No token provided']); +} +``` + +## Token Expiration + +### Setting Expiration Time + +```php +createAuthToken($login, $password, $jwtWrapper, 900); + +// 1 hour +$token = $users->createAuthToken($login, $password, $jwtWrapper, 3600); + +// 24 hours +$token = $users->createAuthToken($login, $password, $jwtWrapper, 86400); + +// 7 days +$token = $users->createAuthToken($login, $password, $jwtWrapper, 604800); +``` + +### Refresh Tokens + +For long-lived sessions, implement a refresh token pattern: + +```php +createAuthToken( + $login, + $password, + $jwtWrapper, + 900, // 15 minutes + [], + ['type' => 'access'] +); + +// Create long-lived refresh token +$refreshToken = $users->createAuthToken( + $login, + $password, + $jwtWrapperRefresh, // Different wrapper/key + 604800, // 7 days + [], + ['type' => 'refresh'] +); + +echo json_encode([ + 'access_token' => $accessToken, + 'refresh_token' => $refreshToken +]); +``` + +## Security Best Practices + +1. **Use HTTPS** - Always transmit tokens over HTTPS +2. **Short expiration times** - Use short-lived tokens (15-60 minutes) +3. **Implement refresh tokens** - For longer sessions +4. **Validate on every request** - Don't trust the client +5. **Store securely** - Don't store tokens in localStorage if possible +6. **Include audience claims** - Limit token usage scope +7. **Monitor for abuse** - Track token usage patterns +8. **Rotate secrets** - Periodically rotate JWT secrets + +## Common Pitfalls + +❌ **Don't store sensitive data in JWT payload** - It's not encrypted, only signed + +❌ **Don't use weak secret keys** - Use cryptographically random keys + +❌ **Don't skip expiration** - Always set reasonable expiration times + +❌ **Don't forget to invalidate** - Provide logout functionality + +❌ **Don't use HTTP** - Always use HTTPS in production + +## Next Steps + +- [Authentication](authentication.md) - Other authentication methods +- [Session Context](session-context.md) - Session-based authentication +- [User Properties](user-properties.md) - Managing user data diff --git a/docs/mappers.md b/docs/mappers.md new file mode 100644 index 0000000..26e693d --- /dev/null +++ b/docs/mappers.md @@ -0,0 +1,433 @@ +--- +sidebar_position: 11 +title: Mappers and Entity Processors +--- + +# Mappers and Entity Processors + +Mappers and Entity Processors allow you to transform data as it's read from or written to the database. + +## What Are Mappers? + +Mappers implement the `MapperFunctionInterface` and transform individual field values during database operations. + +- **Update Mappers**: Transform values **before** saving to database +- **Select Mappers**: Transform values **after** reading from database + +## Built-in Mappers + +### PasswordSha1Mapper + +Automatically hashes passwords using SHA-1: + +```php +defineMapperForUpdate( + UserDefinition::FIELD_PASSWORD, + PasswordSha1Mapper::class +); +``` + +### StandardMapper + +Default mapper that passes values through unchanged: + +```php +defineMapperForUpdate('name', StandardMapper::class); +``` + +### ReadOnlyMapper + +Prevents field updates: + +```php +markPropertyAsReadOnly(UserDefinition::FIELD_CREATED); + +// Or explicitly +$userDefinition->defineMapperForUpdate( + UserDefinition::FIELD_CREATED, + ReadOnlyMapper::class +); +``` + +## Creating Custom Mappers + +### Mapper Interface + +```php + 12]); + } +} + +// Use it +$userDefinition->defineMapperForUpdate( + UserDefinition::FIELD_PASSWORD, + BcryptPasswordMapper::class +); +``` + +### Example: Email Normalization Mapper + +```php +defineMapperForUpdate( + UserDefinition::FIELD_EMAIL, + EmailNormalizationMapper::class +); +``` + +### Example: JSON Serialization Mappers + +```php +defineMapperForUpdate('preferences', JsonEncodeMapper::class); +$userDefinition->defineMapperForSelect('preferences', JsonDecodeMapper::class); +``` + +### Example: Date Formatting Mapper + +```php +format('Y-m-d H:i:s'); + } + if (is_string($value)) { + return $value; + } + if (is_int($value)) { + return date('Y-m-d H:i:s', $value); + } + return $value; + } +} + +class DateParseMapper implements MapperFunctionInterface +{ + public function processedValue(mixed $value, mixed $instance): mixed + { + if (empty($value)) { + return null; + } + try { + return new \DateTime($value); + } catch (\Exception $e) { + return $value; + } + } +} + +$userDefinition->defineMapperForUpdate('created', DateFormatMapper::class); +$userDefinition->defineMapperForSelect('created', DateParseMapper::class); +``` + +## Entity Processors + +Entity Processors transform the **entire entity** (UserModel) before insert or update operations. + +### Entity Processor Interface + +```php +setBeforeInsert(new PassThroughEntityProcessor()); +``` + +### Custom Entity Processors + +#### Example: Auto-Set Created Timestamp + +```php +getCreated())) { + $instance->setCreated(date('Y-m-d H:i:s')); + } + } + } +} + +$userDefinition->setBeforeInsert(new CreatedTimestampProcessor()); +``` + +#### Example: Username Validation + +```php +getUsername(); + + if (strlen($username) < 3) { + throw new \InvalidArgumentException('Username must be at least 3 characters'); + } + + if (!preg_match('/^[a-zA-Z0-9_]+$/', $username)) { + throw new \InvalidArgumentException('Username can only contain letters, numbers, and underscores'); + } + } + } +} + +$userDefinition->setBeforeInsert(new UsernameValidationProcessor()); +$userDefinition->setBeforeUpdate(new UsernameValidationProcessor()); +``` + +#### Example: Audit Trail + +```php +userId = $userId; + } + + public function process(mixed $instance): void + { + if ($instance instanceof UserModel) { + $instance->set('modified_by', $this->userId); + $instance->set('modified_at', date('Y-m-d H:i:s')); + } + } +} + +$userDefinition->setBeforeUpdate(new AuditProcessor($currentUserId)); +``` + +## Using Closures (Legacy) + +For backward compatibility, you can use closures instead of dedicated mapper classes: + +```php +defineMapperForUpdate( + UserDefinition::FIELD_EMAIL, + new ClosureMapper(function ($value, $instance) { + return strtolower(trim($value)); + }) +); + +// Select mapper +$userDefinition->defineMapperForSelect( + UserDefinition::FIELD_CREATED, + new ClosureMapper(function ($value, $instance) { + return date('Y', strtotime($value)); + }) +); +``` + +:::warning Deprecated Methods +The following methods are deprecated but still work: +- `defineClosureForUpdate()` - Use `defineMapperForUpdate()` with `ClosureMapper` +- `defineClosureForSelect()` - Use `defineMapperForSelect()` with `ClosureMapper` +- `getClosureForUpdate()` - Use `getMapperForUpdate()` +- `getClosureForSelect()` - Use `getMapperForSelect()` +::: + +## Complete Example + +```php +getCreated())) { + $instance->setCreated(date('Y-m-d H:i:s')); + } + if (empty($instance->getAdmin())) { + $instance->setAdmin('no'); + } + } + } +} + +// Configure User Definition +$userDefinition = new UserDefinition(); + +// Apply mappers +$userDefinition->defineMapperForUpdate('name', TrimMapper::class); +$userDefinition->defineMapperForUpdate('email', LowercaseMapper::class); +$userDefinition->defineMapperForUpdate('username', LowercaseMapper::class); + +// Apply entity processors +$userDefinition->setBeforeInsert(new DefaultsProcessor()); + +// Initialize +$users = new UsersDBDataset($dbDriver, $userDefinition); +``` + +## Best Practices + +1. **Keep mappers simple** - Each mapper should do one thing +2. **Chain mappers** - Use composition for complex transformations +3. **Handle null values** - Always check for null/empty values +4. **Be idempotent** - Applying mapper multiple times should be safe +5. **Use entity processors for validation** - Validate complete entities +6. **Document side effects** - Make it clear what each mapper does + +## Next Steps + +- [Custom Fields](custom-fields.md) - Extending UserModel +- [Password Validation](password-validation.md) - Password policies +- [Database Storage](database-storage.md) - Schema configuration diff --git a/docs/password-validation.md b/docs/password-validation.md new file mode 100644 index 0000000..8df36b7 --- /dev/null +++ b/docs/password-validation.md @@ -0,0 +1,288 @@ +--- +sidebar_position: 8 +title: Password Validation +--- + +# Password Validation + +The `PasswordDefinition` class provides comprehensive password strength validation and generation capabilities. + +## Basic Usage + +### Creating a Password Definition + +```php +withPasswordDefinition($passwordDefinition); + +// Now password is validated when set +$userModel->setPassword('WeakPwd'); // Throws InvalidArgumentException +``` + +## Password Rules + +### Default Rules + +The default password policy requires: + +| Rule | Default Value | Description | +|---------------------|---------------|------------------------------------------| +| `minimum_chars` | 8 | Minimum password length | +| `require_uppercase` | 0 | Number of uppercase letters required | +| `require_lowercase` | 1 | Number of lowercase letters required | +| `require_symbols` | 0 | Number of symbols required | +| `require_numbers` | 1 | Number of digits required | +| `allow_whitespace` | 0 | Allow whitespace characters (0 = no) | +| `allow_sequential` | 0 | Allow sequential characters (0 = no) | +| `allow_repeated` | 0 | Allow repeated patterns (0 = no) | + +### Custom Rules + +```php + 12, + PasswordDefinition::REQUIRE_UPPERCASE => 2, + PasswordDefinition::REQUIRE_LOWERCASE => 2, + PasswordDefinition::REQUIRE_SYMBOLS => 1, + PasswordDefinition::REQUIRE_NUMBERS => 2, + PasswordDefinition::ALLOW_WHITESPACE => 0, + PasswordDefinition::ALLOW_SEQUENTIAL => 0, + PasswordDefinition::ALLOW_REPEATED => 0 +]); +``` + +### Setting Individual Rules + +```php +setRule(PasswordDefinition::MINIMUM_CHARS, 10); +$passwordDefinition->setRule(PasswordDefinition::REQUIRE_UPPERCASE, 1); +$passwordDefinition->setRule(PasswordDefinition::REQUIRE_SYMBOLS, 1); +``` + +## Validating Passwords + +### Validation Result Codes + +The `matchPassword()` method returns a bitwise result: + +```php +matchPassword('weak'); + +if ($result === PasswordDefinition::SUCCESS) { + echo "Password is valid"; +} else { + // Check specific failures + if ($result & PasswordDefinition::FAIL_MINIMUM_CHARS) { + echo "Password is too short\n"; + } + if ($result & PasswordDefinition::FAIL_UPPERCASE) { + echo "Missing uppercase letters\n"; + } + if ($result & PasswordDefinition::FAIL_LOWERCASE) { + echo "Missing lowercase letters\n"; + } + if ($result & PasswordDefinition::FAIL_NUMBERS) { + echo "Missing numbers\n"; + } + if ($result & PasswordDefinition::FAIL_SYMBOLS) { + echo "Missing symbols\n"; + } + if ($result & PasswordDefinition::FAIL_WHITESPACE) { + echo "Whitespace not allowed\n"; + } + if ($result & PasswordDefinition::FAIL_SEQUENTIAL) { + echo "Sequential characters detected\n"; + } + if ($result & PasswordDefinition::FAIL_REPEATED) { + echo "Repeated patterns detected\n"; + } +} +``` + +### Available Failure Codes + +| Constant | Value | Description | +|---------------------------|-------|----------------------------------| +| `SUCCESS` | 0 | Password is valid | +| `FAIL_MINIMUM_CHARS` | 1 | Password too short | +| `FAIL_UPPERCASE` | 2 | Missing uppercase letters | +| `FAIL_LOWERCASE` | 4 | Missing lowercase letters | +| `FAIL_SYMBOLS` | 8 | Missing symbols | +| `FAIL_NUMBERS` | 16 | Missing numbers | +| `FAIL_WHITESPACE` | 32 | Whitespace not allowed | +| `FAIL_SEQUENTIAL` | 64 | Sequential characters detected | +| `FAIL_REPEATED` | 128 | Repeated patterns detected | + +## Password Generation + +### Generate a Random Password + +```php +generatePassword(); +echo $password; // e.g., "aB3dE7fG9" +``` + +### Generate Longer Passwords + +```php +generatePassword(5); +``` + +The generated password will: +- Meet all defined rules +- Be cryptographically random +- Avoid sequential and repeated patterns + +## User Registration with Password Validation + +### Complete Example + +```php + 10, + PasswordDefinition::REQUIRE_UPPERCASE => 1, + PasswordDefinition::REQUIRE_LOWERCASE => 1, + PasswordDefinition::REQUIRE_SYMBOLS => 1, + PasswordDefinition::REQUIRE_NUMBERS => 1, +]); + +// Create user with password validation +try { + $user = new UserModel(); + $user->withPasswordDefinition($passwordDefinition); + + $user->setName('John Doe'); + $user->setEmail('john@example.com'); + $user->setUsername('johndoe'); + $user->setPassword($_POST['password']); // Validated automatically + + $users->save($user); + echo "User created successfully"; + +} catch (InvalidArgumentException $e) { + echo "Password validation failed: " . $e->getMessage(); +} +``` + +## User-Friendly Error Messages + +```php +matchPassword($_POST['password']); +if ($result !== PasswordDefinition::SUCCESS) { + $errors = getPasswordErrors($result); + foreach ($errors as $error) { + echo "- " . $error . "\n"; + } +} +``` + +## Sequential and Repeated Patterns + +### Sequential Characters + +Sequential patterns that are detected include: +- **Alphabetic**: abc, bcd, cde, xyz, etc. (case-insensitive) +- **Numeric**: 012, 123, 234, 789, 890, etc. +- **Reverse**: 987, 876, 765, 321, etc. + +### Repeated Patterns + +Repeated patterns include: +- **Repeated characters**: aaa, 111, etc. +- **Repeated sequences**: ababab, 123123, etc. + +## Password Change Flow + +```php +getById($userId); + $user->withPasswordDefinition($passwordDefinition); + + // Verify old password + $existingUser = $users->isValidUser($user->getUsername(), $_POST['old_password']); + if ($existingUser === null) { + throw new Exception("Current password is incorrect"); + } + + // Set new password (validated automatically) + $user->setPassword($_POST['new_password']); + $users->save($user); + + echo "Password changed successfully"; + +} catch (InvalidArgumentException $e) { + echo "New password validation failed: " . $e->getMessage(); +} +``` + +## Best Practices + +1. **Balance security and usability** - Don't make rules too restrictive +2. **Educate users** - Provide clear error messages +3. **Use password generation** - Offer to generate strong passwords +4. **Consider passphrases** - Allow longer passwords with spaces if appropriate +5. **Combine with rate limiting** - Prevent brute force attacks + +## Next Steps + +- [Authentication](authentication.md) - Validating credentials +- [User Management](user-management.md) - Managing users +- [Mappers](mappers.md) - Custom password hashing diff --git a/docs/session-context.md b/docs/session-context.md new file mode 100644 index 0000000..d0f9290 --- /dev/null +++ b/docs/session-context.md @@ -0,0 +1,197 @@ +--- +sidebar_position: 5 +title: Session Context +--- + +# Session Context + +The `SessionContext` class manages user authentication state using PSR-6 compatible cache storage. + +## Creating a Session Context + +```php +registerLogin($userId); + +// With additional session data +$sessionContext->registerLogin($userId, ['ip' => $_SERVER['REMOTE_ADDR']]); +``` + +### Check Authentication Status + +```php +isAuthenticated()) { + echo "User is logged in"; +} else { + echo "User is not authenticated"; +} +``` + +### Get Current User Info + +```php +isAuthenticated()) { + $userId = $sessionContext->userInfo(); + // Use $userId to fetch user details +} +``` + +### Logout + +```php +registerLogout(); +``` + +## Storing Session Data + +You can store custom data in the user's session. This data exists only while the user is logged in. + +### Store Data + +```php +setSessionData('shopping_cart', [ + 'item1' => 'Product A', + 'item2' => 'Product B' +]); + +$sessionContext->setSessionData('last_page', '/products'); +``` + +:::warning Authentication Required +The user must be authenticated to use `setSessionData()`. If not, a `NotAuthenticatedException` will be thrown. +::: + +### Retrieve Data + +```php +getSessionData('shopping_cart'); +$lastPage = $sessionContext->getSessionData('last_page'); +``` + +Returns `false` if: +- The user is not authenticated +- The key doesn't exist + +### Session Data Lifecycle + +- Session data is stored when the user logs in +- It persists across requests while the user remains logged in +- It is automatically deleted when the user logs out +- It is lost if the session expires + +## Complete Example + +```php +isValidUser($_POST['username'], $_POST['password']); + + if ($user !== null) { + $sessionContext->registerLogin($user->getUserid()); + $sessionContext->setSessionData('login_time', time()); + header('Location: /dashboard'); + exit; + } +} + +// Protected pages +if (!$sessionContext->isAuthenticated()) { + header('Location: /login'); + exit; +} + +$userId = $sessionContext->userInfo(); +$user = $users->getById($userId); +$loginTime = $sessionContext->getSessionData('login_time'); + +echo "Welcome, " . $user->getName(); +echo "Logged in at: " . date('Y-m-d H:i:s', $loginTime); + +// Logout +if (isset($_POST['logout'])) { + $sessionContext->registerLogout(); + header('Location: /login'); + exit; +} +``` + +## Best Practices + +1. **Use PHP Session storage** unless you have specific requirements for distributed sessions +2. **Always check authentication** before accessing protected resources +3. **Clear sensitive session data** when no longer needed +4. **Set appropriate session timeouts** based on your security requirements +5. **Regenerate session IDs** after login to prevent session fixation attacks + +## Next Steps + +- [Authentication](authentication.md) - User authentication methods +- [User Properties](user-properties.md) - Store persistent user data diff --git a/docs/user-management.md b/docs/user-management.md new file mode 100644 index 0000000..3ed82a4 --- /dev/null +++ b/docs/user-management.md @@ -0,0 +1,154 @@ +--- +sidebar_position: 3 +title: User Management +--- + +# User Management + +## Creating Users + +### Using addUser() Method + +The simplest way to add a user: + +```php +addUser( + 'John Doe', // Full name + 'johndoe', // Username + 'john@example.com', // Email + 'SecurePass123' // Password +); +``` + +### Using UserModel + +For more control, create a `UserModel` instance: + +```php +setName('John Doe'); +$userModel->setUsername('johndoe'); +$userModel->setEmail('john@example.com'); +$userModel->setPassword('SecurePass123'); +$userModel->setAdmin('no'); + +$savedUser = $users->save($userModel); +``` + +## Retrieving Users + +### Get User by ID + +```php +getById($userId); +``` + +### Get User by Email + +```php +getByEmail('john@example.com'); +``` + +### Get User by Username + +```php +getByUsername('johndoe'); +``` + +### Get User by Login Field + +The login field is determined by the `UserDefinition` (either email or username): + +```php +getByLoginField('johndoe'); +``` + +### Using Custom Filters + +For advanced queries, use `IteratorFilter`: + +```php +and('email', Relation::EQUAL, 'john@example.com'); +$filter->and('admin', Relation::EQUAL, 'yes'); + +$user = $users->getUser($filter); +``` + +## Updating Users + +```php +getById($userId); + +// Update fields +$user->setName('Jane Doe'); +$user->setEmail('jane@example.com'); + +// Save changes +$users->save($user); +``` + +## Deleting Users + +### Delete by ID + +```php +removeUserById($userId); +``` + +### Delete by Login + +```php +removeByLoginField('johndoe'); +``` + +## Checking Admin Status + +```php +isAdmin($userId)) { + echo "User is an administrator"; +} +``` + +The admin field accepts the following values as `true`: +- `yes`, `YES`, `y`, `Y` +- `true`, `TRUE`, `t`, `T` +- `1` +- `s`, `S` (from Portuguese "sim") + +## UserModel Properties + +The `UserModel` class provides the following properties: + +| Property | Type | Description | +|------------|---------------------|--------------------------------| +| userid | string\|int\|null | User ID (auto-generated) | +| name | string\|null | User's full name | +| email | string\|null | User's email address | +| username | string\|null | User's username | +| password | string\|null | User's password (hashed) | +| created | string\|null | Creation timestamp | +| admin | string\|null | Admin flag (yes/no) | + +## Next Steps + +- [Authentication](authentication.md) - Validate user credentials +- [User Properties](user-properties.md) - Store custom user data +- [Password Validation](password-validation.md) - Enforce password policies diff --git a/docs/user-properties.md b/docs/user-properties.md new file mode 100644 index 0000000..1666821 --- /dev/null +++ b/docs/user-properties.md @@ -0,0 +1,248 @@ +--- +sidebar_position: 6 +title: User Properties +--- + +# User Properties + +User properties allow you to store custom key-value data associated with users. This is useful for storing additional information beyond the standard user fields. + +## Adding Properties + +### Add a Single Property + +```php +addProperty($userId, 'phone', '555-1234'); +$users->addProperty($userId, 'department', 'Engineering'); +``` + +:::info Duplicates +`addProperty()` will not add the property if it already exists with the same value. +::: + +### Add Multiple Values for the Same Property + +Users can have multiple values for the same property: + +```php +addProperty($userId, 'role', 'developer'); +$users->addProperty($userId, 'role', 'manager'); +``` + +### Set a Property (Update or Create) + +Use `setProperty()` to update an existing property or create it if it doesn't exist: + +```php +setProperty($userId, 'phone', '555-5678'); +``` + +## Using UserModel + +You can also manage properties directly through the `UserModel`: + +```php +getById($userId); + +// Set a property value +$user->set('phone', '555-1234'); + +// Add a property model +$property = new UserPropertiesModel('department', 'Engineering'); +$user->addProperty($property); + +// Save the user to persist properties +$users->save($user); +``` + +## Retrieving Properties + +### Get a Single Property + +```php +getProperty($userId, 'phone'); +// Returns: '555-1234' +``` + +### Get Multiple Values + +If a property has multiple values, an array is returned: + +```php +getProperty($userId, 'role'); +// Returns: ['developer', 'manager'] +``` + +Returns `null` if the property doesn't exist. + +### Get Properties from UserModel + +```php +getById($userId); + +// Get property value(s) +$phone = $user->get('phone'); + +// Get property as UserPropertiesModel instance +$propertyModel = $user->get('phone', true); + +// Get all properties +$allProperties = $user->getProperties(); +foreach ($allProperties as $property) { + echo $property->getName() . ': ' . $property->getValue(); +} +``` + +## Checking Properties + +### Check if User Has a Property + +```php +hasProperty($userId, 'phone')) { + echo "User has a phone number"; +} + +// Check if property has a specific value +if ($users->hasProperty($userId, 'role', 'admin')) { + echo "User is an admin"; +} +``` + +:::tip Admin Bypass +The `hasProperty()` method always returns `true` for admin users, regardless of the actual property values. +::: + +## Removing Properties + +### Remove a Specific Property Value + +```php +removeProperty($userId, 'role', 'developer'); +``` + +### Remove All Values of a Property + +```php +removeProperty($userId, 'phone'); +``` + +### Remove Property from All Users + +```php +removeAllProperties('temporary_flag'); + +// Remove a specific value from all users +$users->removeAllProperties('role', 'guest'); +``` + +## Finding Users by Properties + +### Find Users with a Specific Property Value + +```php +getUsersByProperty('department', 'Engineering'); +// Returns array of UserModel objects +``` + +### Find Users with Multiple Properties + +```php +getUsersByPropertySet([ + 'department' => 'Engineering', + 'role' => 'senior', + 'status' => 'active' +]); +// Returns users that have ALL these properties with the specified values +``` + +## Common Use Cases + +### User Roles and Permissions + +```php +addProperty($userId, 'role', 'viewer'); +$users->addProperty($userId, 'role', 'editor'); +$users->addProperty($userId, 'role', 'admin'); + +// Check permissions +if ($users->hasProperty($userId, 'role', 'admin')) { + // Allow admin actions +} + +// Get all roles +$roles = $users->getProperty($userId, 'role'); +``` + +### User Preferences + +```php +setProperty($userId, 'theme', 'dark'); +$users->setProperty($userId, 'language', 'en'); +$users->setProperty($userId, 'timezone', 'America/New_York'); + +// Retrieve preferences +$theme = $users->getProperty($userId, 'theme'); +``` + +### Multi-tenant Applications + +```php +addProperty($userId, 'organization', 'org-123'); +$users->addProperty($userId, 'organization', 'org-456'); + +// Find all users in an organization +$orgUsers = $users->getUsersByProperty('organization', 'org-123'); + +// Check access +if ($users->hasProperty($userId, 'organization', $requestedOrgId)) { + // Grant access +} +``` + +## Property Storage + +### Database Storage + +Properties are stored in a separate table (default: `users_property`): + +| Column | Description | +|------------|--------------------------| +| customid | Property ID | +| userid | User ID (foreign key) | +| name | Property name | +| value | Property value | + +### XML/AnyDataset Storage + +Properties are stored as fields within each user's record, with arrays used for multiple values. + +## Next Steps + +- [User Management](user-management.md) - Basic user operations +- [Database Storage](database-storage.md) - Configure property storage +- [Custom Fields](custom-fields.md) - Extend the UserModel diff --git a/example.php b/example.php index 99f0e14..49c2da6 100644 --- a/example.php +++ b/example.php @@ -2,20 +2,41 @@ require "vendor/autoload.php"; -$users = new ByJG\Authenticate\UsersAnyDataset('/tmp/pass.anydata.xml'); +use ByJG\Authenticate\UsersAnyDataset; +use ByJG\Authenticate\SessionContext; +use ByJG\AnyDataset\Core\AnyDataset; +use ByJG\Cache\Factory; -$users->addUser('Some User Full Name', 'someuser', 'someuser@someemail.com', '12345'); -//$users->save(); +// Create or load AnyDataset from XML file +$anyDataset = new AnyDataset('/tmp/users.xml'); -$user = $users->isValidUser('someuser', '12345'); -var_dump($user); -if (!is_null($user)) -{ - $session = new \ByJG\Authenticate\SessionContext(); - $session->registerLogin($userId); +// Initialize user management +$users = new UsersAnyDataset($anyDataset); - echo "Authenticated: " . $session->isAuthenticated(); - print_r($session->userInfo()); +// Add a new user +$user = $users->addUser('Some User Full Name', 'someuser', 'someuser@someemail.com', '12345'); +echo "User created with ID: " . $user->getUserid() . "\n"; + +// Validate user credentials +$authenticatedUser = $users->isValidUser('someuser', '12345'); +var_dump($authenticatedUser); + +if ($authenticatedUser !== null) { + // Create session context + $session = new SessionContext(Factory::createSessionPool()); + + // Register login + $session->registerLogin($authenticatedUser->getUserid()); + + echo "Authenticated: " . ($session->isAuthenticated() ? 'yes' : 'no') . "\n"; + echo "User ID: " . $session->userInfo() . "\n"; + + // Store some session data + $session->setSessionData('login_time', time()); + + // Get the user info + $currentUser = $users->getById($session->userInfo()); + echo "Welcome, " . $currentUser->getName() . "\n"; } From 421ede17f9eed22c53f7e7b68dd679a71481604d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Nov 2025 20:42:07 +0000 Subject: [PATCH 2/6] Move Dependencies section to end of README before footer --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b973e33..d610074 100644 --- a/README.md +++ b/README.md @@ -289,6 +289,10 @@ Because this project uses PHP Session you need to run the unit test the followin └───────────────────┘ └───────────────────┘ └────────────────────┘ ``` +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + ## Dependencies ```mermaid @@ -298,9 +302,5 @@ flowchart TD byjg/authuser --> byjg/jwt-wrapper ``` -## License - -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. - ---- [Open source ByJG](http://opensource.byjg.com) From 1e5e4b5fa9349485723cd45ad89cc1946df09f4f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Nov 2025 20:48:14 +0000 Subject: [PATCH 3/6] Apply DRY pattern to documentation Consolidated duplicate content and improved documentation structure: README.md changes: - Removed duplicate installation examples - Simplified Basic Usage to single example - Removed detailed sections (now in specific docs) - Replaced with Features section linking to docs - Simplified Dependencies section docs/getting-started.md changes: - Removed duplicate Key Features list - Simplified Quick Example code docs/authentication.md changes: - Added cross-reference to Password Validation docs/custom-fields.md changes: - Added clarity on when to use vs Database Storage Benefits: - Single source of truth for each concept - Clear navigation through cross-references - Better maintainability --- README.md | 231 ++++------------------------------------ docs/authentication.md | 6 +- docs/custom-fields.md | 4 + docs/getting-started.md | 28 +---- 4 files changed, 36 insertions(+), 233 deletions(-) diff --git a/README.md b/README.md index d610074..ba2dd87 100644 --- a/README.md +++ b/README.md @@ -34,229 +34,44 @@ This class can persist user data into session (or file, memcache, etc.) between composer require byjg/authuser ``` -### Using Database Storage - -```php -addUser('John Doe', 'johndoe', 'john@example.com', 'SecurePass123'); +// Initialize with database +$users = new UsersDBDataset(DbFactory::getDbInstance('mysql://user:pass@host/db')); -// Validate user credentials +// Create and authenticate a user +$user = $users->addUser('John Doe', 'johndoe', 'john@example.com', 'SecurePass123'); $authenticatedUser = $users->isValidUser('johndoe', 'SecurePass123'); if ($authenticatedUser !== null) { - // Create session context $sessionContext = new SessionContext(Factory::createSessionPool()); - - // Register the login $sessionContext->registerLogin($authenticatedUser->getUserid()); - echo "Welcome, " . $authenticatedUser->getName(); } ``` -### Check if User is Authenticated - -```php -isAuthenticated()) { - // Get the userId of the authenticated user - $userId = $sessionContext->userInfo(); - - // Get the user and display name - $user = $users->getById($userId); - echo "Hello: " . $user->getName(); -} else { - echo "Please log in"; -} -``` - -## Managing Session Data - -You can store temporary data in the user session that exists only while the user is logged in. Once the user logs out, the data is automatically released. - -### Store Session Data - -```php -setSessionData('shopping_cart', [ - 'item1' => 'Product A', - 'item2' => 'Product B' -]); - -$sessionContext->setSessionData('last_page', '/products'); -``` - -### Retrieve Session Data - -```php -getSessionData('shopping_cart'); -$lastPage = $sessionContext->getSessionData('last_page'); -``` - -:::note -A `NotAuthenticatedException` will be thrown if the user is not authenticated when accessing session data. -::: - -## Managing User Properties - -User properties allow you to store custom key-value data associated with users permanently. - -### Add Custom Properties - -```php -addProperty($userId, 'phone', '555-1234'); -$users->addProperty($userId, 'department', 'Engineering'); - -// Users can have multiple values for the same property -$users->addProperty($userId, 'role', 'developer'); -$users->addProperty($userId, 'role', 'manager'); -``` - -### Using UserModel - -```php -getById($userId); - -// Set a property (update or create) -$user->set('phone', '555-1234'); - -// Save changes -$users->save($user); -``` - -## Logout - -```php -registerLogout(); -``` - -## JWT Token Authentication - -For stateless API authentication, you can use JWT tokens: - -```php -createAuthToken( - 'johndoe', // Login - 'SecurePass123', // Password - $jwtWrapper, - 3600, // Expires in 1 hour (seconds) - [], // Additional user info to save - ['role' => 'admin'] // Additional token data -); - -// Validate token -$result = $users->isValidToken('johndoe', $jwtWrapper, $token); -if ($result !== null) { - $user = $result['user']; - $tokenData = $result['data']; -} -``` - -See [JWT Tokens](docs/jwt-tokens.md) for detailed information. - -## Database Schema - -The default database schema uses two tables: - -```sql -CREATE TABLE users ( - userid INTEGER AUTO_INCREMENT NOT NULL, - name VARCHAR(50), - email VARCHAR(120), - username VARCHAR(15) NOT NULL, - password CHAR(40) NOT NULL, - created DATETIME, - admin ENUM('Y','N'), - CONSTRAINT pk_users PRIMARY KEY (userid) -) ENGINE=InnoDB; - -CREATE TABLE users_property ( - customid INTEGER AUTO_INCREMENT NOT NULL, - name VARCHAR(20), - value VARCHAR(100), - userid INTEGER NOT NULL, - CONSTRAINT pk_custom PRIMARY KEY (customid), - CONSTRAINT fk_custom_user FOREIGN KEY (userid) REFERENCES users (userid) -) ENGINE=InnoDB; -``` - -You can customize table and column names using `UserDefinition` and `UserPropertiesDefinition`. See [Database Storage](docs/database-storage.md) for details. +See [Getting Started](docs/getting-started.md) for a complete introduction and [Examples](docs/examples.md) for more use cases. ## Features -- **Complete User Management** - Create, read, update, and delete users -- **Flexible Authentication** - Username/email + password or JWT tokens -- **Session Management** - PSR-6 compatible cache storage -- **User Properties** - Store custom key-value data per user -- **Password Validation** - Built-in password strength requirements -- **Multiple Storage Backends** - Database (MySQL, PostgreSQL, SQLite, etc.) or XML files -- **Customizable Schema** - Map to existing database tables -- **Field Mappers** - Transform data during read/write operations -- **Extensible User Model** - Add custom fields easily +- **User Management** - Complete CRUD operations. See [User Management](docs/user-management.md) +- **Authentication** - Username/email + password or JWT tokens. See [Authentication](docs/authentication.md) and [JWT Tokens](docs/jwt-tokens.md) +- **Session Management** - PSR-6 compatible cache storage. See [Session Context](docs/session-context.md) +- **User Properties** - Store custom key-value metadata. See [User Properties](docs/user-properties.md) +- **Password Validation** - Built-in strength requirements. See [Password Validation](docs/password-validation.md) +- **Multiple Storage** - Database (MySQL, PostgreSQL, SQLite, etc.) or XML files. See [Database Storage](docs/database-storage.md) +- **Custom Schema** - Map to existing database tables. See [Database Storage](docs/database-storage.md) +- **Field Mappers** - Transform data during read/write. See [Mappers](docs/mappers.md) +- **Extensible Model** - Add custom fields easily. See [Custom Fields](docs/custom-fields.md) ## Running Tests @@ -295,12 +110,12 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file ## Dependencies -```mermaid -flowchart TD - byjg/authuser --> byjg/micro-orm - byjg/authuser --> byjg/cache-engine - byjg/authuser --> byjg/jwt-wrapper -``` +This library depends on: +- **byjg/micro-orm** - For database operations +- **byjg/cache-engine** - For session management +- **byjg/jwt-wrapper** - For JWT token support + +See [Installation](docs/installation.md) for details and dependency graph. ---- [Open source ByJG](http://opensource.byjg.com) diff --git a/docs/authentication.md b/docs/authentication.md index b18cc31..18f756e 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -39,7 +39,11 @@ $users->save($user); ``` :::warning SHA-1 Deprecation -SHA-1 is used for backward compatibility. For new projects, consider implementing a custom password hasher using bcrypt or Argon2. See [Custom Mappers](mappers.md) for details. +SHA-1 is used for backward compatibility. For new projects, consider implementing a custom password hasher using bcrypt or Argon2. See [Mappers](mappers.md#example-bcrypt-password-mapper) for details. +::: + +:::tip Enforce Password Strength +To enforce password policies (minimum length, complexity rules, etc.), see [Password Validation](password-validation.md). ::: ## Workflow diff --git a/docs/custom-fields.md b/docs/custom-fields.md index 04d2bc5..050c6e8 100644 --- a/docs/custom-fields.md +++ b/docs/custom-fields.md @@ -7,6 +7,10 @@ title: Custom Fields You can extend the `UserModel` to add custom fields that match your database schema. +:::info When to Use This +This guide is for **adding new fields** beyond the standard user fields. If you just need to **map existing database columns** to the standard fields, see [Database Storage](database-storage.md#custom-database-schema) instead. +::: + ## Extending UserModel ### Creating a Custom User Model diff --git a/docs/getting-started.md b/docs/getting-started.md index aaa8bf6..fecc7c9 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -7,45 +7,25 @@ title: Getting Started Auth User PHP is a simple and customizable library for user authentication in PHP applications. It provides an abstraction layer for managing users, authentication, and user properties, supporting multiple storage backends including databases and XML files. -## Key Features - -- **User Management**: Complete CRUD operations for users -- **Authentication**: Validate user credentials and manage sessions -- **User Properties**: Store and retrieve custom user properties -- **JWT Support**: Create and validate JWT tokens for stateless authentication -- **Password Validation**: Built-in password strength validation -- **Flexible Storage**: Support for databases (via AnyDataset) and XML files -- **Session Management**: PSR-6 compatible cache for session storage - ## Quick Example -Here's a quick example of how to use the library: - ```php addUser('John Doe', 'johndoe', 'john@example.com', 'SecurePass123'); - -// Validate user credentials $user = $users->isValidUser('johndoe', 'SecurePass123'); if ($user !== null) { - // Create a session $sessionContext = new SessionContext(Factory::createSessionPool()); $sessionContext->registerLogin($user->getUserid()); - echo "User authenticated successfully!"; } ``` From e9b21a22116d63a822ee811f0e6a1ec87adaec5e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Nov 2025 21:15:30 +0000 Subject: [PATCH 4/6] Update Dependencies section to show only mermaid diagram - Removed text descriptions from Dependencies section - Now shows only mermaid flowchart with project dependencies - Dependencies sourced from composer.json: * byjg/authuser --> byjg/micro-orm * byjg/authuser --> byjg/cache-engine * byjg/authuser --> byjg/jwt-wrapper - Section remains as last section before footer --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ba2dd87..a45a321 100644 --- a/README.md +++ b/README.md @@ -110,12 +110,12 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file ## Dependencies -This library depends on: -- **byjg/micro-orm** - For database operations -- **byjg/cache-engine** - For session management -- **byjg/jwt-wrapper** - For JWT token support - -See [Installation](docs/installation.md) for details and dependency graph. +```mermaid +flowchart TD + byjg/authuser --> byjg/micro-orm + byjg/authuser --> byjg/cache-engine + byjg/authuser --> byjg/jwt-wrapper +``` ---- [Open source ByJG](http://opensource.byjg.com) From f6c5c2e85ee56bf4de5f8af5e43d4cc87bd3d7fb Mon Sep 17 00:00:00 2001 From: Joao M Date: Thu, 6 Nov 2025 15:17:34 -0600 Subject: [PATCH 5/6] Update installation.md by removing dependencies section Removed dependencies section and related information from installation documentation. --- docs/installation.md | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 68852ae..4b46c5c 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -18,25 +18,6 @@ Install the library using Composer: composer require byjg/authuser ``` -## Dependencies - -The library depends on the following packages: - -- `byjg/micro-orm` - For database operations -- `byjg/cache-engine` - For session management -- `byjg/jwt-wrapper` - For JWT token support - -These dependencies are automatically installed by Composer. - -:::info Dependency Graph -```mermaid -flowchart TD - byjg/authuser --> byjg/micro-orm - byjg/authuser --> byjg/cache-engine - byjg/authuser --> byjg/jwt-wrapper -``` -::: - ## Running Tests Because this project uses PHP Session, you need to run the unit tests with the `--stderr` flag: From 6b0565356df29f91d9652f8f8b12e8c032eac70b Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Thu, 6 Nov 2025 16:27:39 -0500 Subject: [PATCH 6/6] Update authentication docs to recommend JWT-based authentication, deprecate `SessionContext` usage, and clarify differences for security and scalability. --- docs/authentication.md | 104 ++++++++++++++++++++++------------------- 1 file changed, 56 insertions(+), 48 deletions(-) diff --git a/docs/authentication.md b/docs/authentication.md index 18f756e..7ba0842 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -46,55 +46,9 @@ SHA-1 is used for backward compatibility. For new projects, consider implementin To enforce password policies (minimum length, complexity rules, etc.), see [Password Validation](password-validation.md). ::: -## Workflow +## JWT Token Authentication (Recommended) -### Basic Authentication Flow - -```php -isValidUser('johndoe', 'SecurePass123'); - -if ($user !== null) { - // 2. Create session context - $sessionContext = new SessionContext(Factory::createSessionPool()); - - // 3. Register login - $sessionContext->registerLogin($user->getUserid()); - - // 4. User is now authenticated - echo "Welcome, " . $user->getName(); -} -``` - -### Checking Authentication Status - -```php -isAuthenticated()) { - $userId = $sessionContext->userInfo(); - $user = $users->getById($userId); - echo "Hello, " . $user->getName(); -} else { - echo "Please log in"; -} -``` - -### Logging Out - -```php -registerLogout(); -``` - -## JWT Token Authentication - -For stateless authentication, you can use JWT tokens: +For modern, stateless authentication, use JWT tokens. This is the **recommended approach** for new applications as it provides better security and scalability. ```php isValidUser('johndoe', 'SecurePass123'); + +if ($user !== null) { + // 2. Create session context + $sessionContext = new SessionContext(Factory::createSessionPool()); + + // 3. Register login + $sessionContext->registerLogin($user->getUserid()); + + // 4. User is now authenticated + echo "Welcome, " . $user->getName(); +} +``` + +### Checking Authentication Status + +```php +isAuthenticated()) { + $userId = $sessionContext->userInfo(); + $user = $users->getById($userId); + echo "Hello, " . $user->getName(); +} else { + echo "Please log in"; +} +``` + +### Logging Out + +```php +registerLogout(); +``` + ## Security Best Practices 1. **Always use HTTPS** in production to prevent credential theft