diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml
index d5322bf..7d2e33a 100644
--- a/.github/workflows/phpunit.yml
+++ b/.github/workflows/phpunit.yml
@@ -12,16 +12,18 @@ on:
jobs:
Build:
runs-on: 'ubuntu-latest'
- container: 'byjg/php:${{ matrix.php-version }}-cli'
+ container:
+ image: 'byjg/php:${{ matrix.php-version }}-cli'
+ options: --user root --privileged
strategy:
matrix:
php-version:
+ - "8.4"
- "8.3"
- "8.2"
- - "8.1"
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- run: composer install
- run: ./vendor/bin/psalm
- run: ./vendor/bin/phpunit --stderr
@@ -33,5 +35,6 @@ jobs:
with:
folder: php
project: ${{ github.event.repository.name }}
- secrets: inherit
+ secrets:
+ DOC_TOKEN: ${{ secrets.DOC_TOKEN }}
diff --git a/.gitignore b/.gitignore
index f495eec..256a0e6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@ vendor
.idea/*
!.idea/runConfigurations
.phpunit.result.cache
+/.claude/settings.local.json
diff --git a/.idea/runConfigurations/Psalm.xml b/.idea/runConfigurations/Psalm.xml
deleted file mode 100644
index 6f80a21..0000000
--- a/.idea/runConfigurations/Psalm.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/runConfigurations/psalm.xml b/.idea/runConfigurations/psalm.xml
new file mode 100644
index 0000000..d9c1b61
--- /dev/null
+++ b/.idea/runConfigurations/psalm.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index a16b060..1a8c726 100644
--- a/README.md
+++ b/README.md
@@ -6,111 +6,93 @@
[](https://opensource.byjg.com/opensource/licensing.html)
[](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 using a clean repository and service layer architecture.
-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();
-
- // Get the user and your name
- $user = $users->getById($userId);
- echo "Hello: " . $user->getName();
+use ByJG\AnyDataset\Db\DatabaseExecutor;
+use ByJG\AnyDataset\Db\Factory as DbFactory;
+use ByJG\Authenticate\Enum\LoginField;
+use ByJG\Authenticate\Model\UserModel;
+use ByJG\Authenticate\Model\UserPropertiesModel;
+use ByJG\Authenticate\Repository\UsersRepository;
+use ByJG\Authenticate\Repository\UserPropertiesRepository;
+use ByJG\Authenticate\Service\UsersService;
+use ByJG\Authenticate\SessionContext;
+use ByJG\Cache\Factory;
+
+// Initialize repositories and service
+$dbDriver = DbFactory::getDbInstance('mysql://user:pass@host/db');
+$db = DatabaseExecutor::using($dbDriver);
+$usersRepo = new UsersRepository($db, UserModel::class);
+$propsRepo = new UserPropertiesRepository($db, UserPropertiesModel::class);
+$users = new UsersService($usersRepo, $propsRepo, LoginField::Username);
+
+// Create and authenticate a user
+$user = $users->addUser('John Doe', 'johndoe', 'john@example.com', 'SecurePass123');
+$authenticatedUser = $users->isValidUser('johndoe', 'SecurePass123');
+
+if ($authenticatedUser !== null) {
+ $sessionContext = new SessionContext(Factory::createSessionPool());
+ $sessionContext->registerLogin($authenticatedUser->getUserid());
+ echo "Welcome, " . $authenticatedUser->getName();
}
```
-## Saving extra info into the user session
-
-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.
-
-Store the data for the current user session:
-
-```php
-setSessionData('key', 'value');
-```
-
-Getting the data from the current user session:
-
-```php
-getSessionData('key');
-```
-
-Note: If the user is not logged an error will be throw
-
-## Adding a custom property to the users
-
-```php
-getById($userId);
-$user->setField('somefield', 'somevalue');
-$users->save();
-```
-
-## Logout from a session
+Set the third constructor argument to `LoginField::Email` if you prefer authenticating users by email instead of username.
-```php
-registerLogout();
-```
+See [Getting Started](docs/getting-started.md) for a complete introduction and [Examples](docs/examples.md) for more use cases.
-## Important note about SessionContext
+## Features
-`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.
+- **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)
+- **Database Storage** - Supports MySQL, PostgreSQL, SQLite, and more. 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)
-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.
+## Running Tests
-Example for memcached:
+Because this project uses PHP Session you need to run the unit test the following manner:
-```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'
- ]
-);
-```
-
-### Adding custom modifiers for read and update
-
-```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
-
-It is possible extending the UserModel table, since you create a new class extending from UserModel to add the new fields.
-
-For example, imagine your table has one field called "otherfield".
-
-You'll have to extend like this:
-
-```php
-setOtherfield($field);
- }
-
- public function getOtherfield()
- {
- return $this->otherfield;
- }
-
- public function setOtherfield($otherfield)
- {
- $this->otherfield = $otherfield;
- }
-}
-```
-
-After that you can use your new definition:
-
-```php
- byjg/micro-orm
byjg/authuser --> byjg/cache-engine
- byjg/authuser --> byjg/jwt-wrapper
+ byjg/authuser --> byjg/jwt-wrapper
```
-
----
[Open source ByJG](http://opensource.byjg.com)
diff --git a/composer.json b/composer.json
index ea629b9..3f5a5d7 100644
--- a/composer.json
+++ b/composer.json
@@ -14,14 +14,18 @@
"minimum-stability": "dev",
"prefer-stable": true,
"require": {
- "php": ">=8.1 <8.4",
- "byjg/micro-orm": "^5.0",
- "byjg/cache-engine": "^5.0",
- "byjg/jwt-wrapper": "^5.0"
+ "php": ">=8.2 <8.5",
+ "byjg/micro-orm": "^6.0",
+ "byjg/cache-engine": "^6.0",
+ "byjg/jwt-wrapper": "^6.0"
},
"require-dev": {
- "phpunit/phpunit": "^9.6",
- "vimeo/psalm": "^5.9"
+ "phpunit/phpunit": "^10|^11",
+ "vimeo/psalm": "^5.9|^6.12"
+ },
+ "scripts": {
+ "test": "vendor/bin/phpunit",
+ "psalm": "vendor/bin/psalm"
},
"license": "MIT"
}
diff --git a/docs/authentication.md b/docs/authentication.md
new file mode 100644
index 0000000..352aa04
--- /dev/null
+++ b/docs/authentication.md
@@ -0,0 +1,192 @@
+---
+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 configured in your `UsersService` constructor. Pass either `LoginField::Email` or `LoginField::Username` when creating the service.
+:::
+`LoginField` lives in the `ByJG\Authenticate\Enum` namespace.
+
+## 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 [Mappers](mappers.md) for details.
+:::
+
+:::tip Enforce Password Strength
+To enforce password policies (minimum length, complexity rules, etc.), see [Password Validation](password-validation.md).
+:::
+
+## JWT Token Authentication (Recommended)
+
+For modern, stateless authentication, use JWT tokens. This is the **recommended approach** for new applications as it provides better security and scalability.
+
+```php
+createAuthToken(
+ 'johndoe', // Login
+ 'SecurePass123', // Password
+ $jwtWrapper,
+ 3600, // Expires in 1 hour (seconds)
+ [], // Additional user info to save
+ ['role' => 'admin'] // Additional token data
+);
+
+if ($userToken !== null) {
+ echo "Token: " . $userToken->token;
+}
+```
+
+Need to include standard user columns (name, email, etc.) automatically? Pass the optional seventh argument with `User` enum values or strings. See [JWT Tokens](jwt-tokens.md#copy-user-fields-automatically) for details.
+
+:::tip UserToken Return Type
+Both `createAuthToken()` and `createInsecureAuthToken()` return a `UserToken` object with three properties:
+- `token` - The JWT token string
+- `user` - The UserModel instance
+- `data` - Array of token payload data
+
+This provides immediate access to both the token and user information without additional database queries.
+:::
+
+### Creating Tokens Without Password Validation
+
+Use `createInsecureAuthToken()` when you need to create tokens without password validation:
+
+```php
+getByEmail($email);
+
+$userToken = $users->createInsecureAuthToken(
+ $user, // Can pass UserModel or login string
+ $jwtWrapper,
+ 3600
+);
+
+echo "Token: " . $userToken->token;
+```
+
+See [JWT Tokens](jwt-tokens.md#creating-tokens-without-password-validation) for complete details and use cases.
+
+### Validating JWT Tokens
+
+```php
+isValidToken('johndoe', $jwtWrapper, $token);
+
+if ($userToken !== null) {
+ $user = $userToken->user;
+ $tokenData = $userToken->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.
+:::
+
+:::tip Why JWT?
+JWT tokens provide stateless authentication, better scalability, and easier integration with modern frontend frameworks and mobile applications. They're also more secure than traditional PHP sessions.
+:::
+
+## Session-Based Authentication
+
+The `SessionContext` class persists the authenticated user ID inside a PSR-6 cache pool. Use this when you need server-managed sessions (for example, classic PHP applications).
+
+### 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();
+```
+
+## 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..845b30d
--- /dev/null
+++ b/docs/custom-fields.md
@@ -0,0 +1,308 @@
+---
+sidebar_position: 10
+title: Custom Fields
+---
+
+# 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
+
+```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_at DATETIME,
+ updated_at DATETIME,
+ deleted_at DATETIME,
+ role VARCHAR(20),
+ -- Custom fields
+ phone VARCHAR(20),
+ department VARCHAR(50),
+ title VARCHAR(50),
+ profile_picture VARCHAR(255),
+
+ CONSTRAINT pk_users PRIMARY KEY (userid)
+) ENGINE=InnoDB;
+```
+
+## Using the Custom Model
+
+### Initializing the Service
+
+```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 using the `ReadOnlyMapper`:
+
+```php
+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..1ffbcfb
--- /dev/null
+++ b/docs/database-storage.md
@@ -0,0 +1,401 @@
+---
+sidebar_position: 7
+title: Database Storage
+---
+
+# Database Storage
+
+The library uses a repository pattern to store users in relational databases through `UsersRepository` and `UserPropertiesRepository`.
+
+## Database Setup
+
+### Default Schema
+
+The default database structure uses two tables. Below are the schema definitions for different databases:
+
+
+MySQL / MariaDB
+
+```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_at DATETIME DEFAULT (now()),
+ updated_at DATETIME ON UPDATE CURRENT_TIMESTAMP,
+ deleted_at DATETIME,
+ role VARCHAR(20),
+
+ CONSTRAINT pk_users PRIMARY KEY (userid),
+ CONSTRAINT ix_username UNIQUE (username),
+ CONSTRAINT ix_users_2 UNIQUE (email),
+ INDEX ix_users_email (email ASC, password ASC),
+ INDEX ix_users_username (username ASC, password ASC)
+) ENGINE=InnoDB;
+
+CREATE TABLE users_property
+(
+ id INTEGER AUTO_INCREMENT NOT NULL,
+ name VARCHAR(20),
+ value VARCHAR(100),
+ userid INTEGER NOT NULL,
+
+ CONSTRAINT pk_custom PRIMARY KEY (id),
+ CONSTRAINT fk_custom_user FOREIGN KEY (userid) REFERENCES users (userid)
+) ENGINE=InnoDB;
+```
+
+
+
+
+SQLite
+
+```sql
+CREATE TABLE users
+(
+ userid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ name VARCHAR(50),
+ email VARCHAR(120),
+ username VARCHAR(15) NOT NULL,
+ password CHAR(40) NOT NULL,
+ created_at DATETIME DEFAULT (datetime('now')),
+ updated_at DATETIME DEFAULT (datetime('now')),
+ deleted_at DATETIME,
+ role VARCHAR(20),
+
+ CONSTRAINT ix_username UNIQUE (username),
+ CONSTRAINT ix_users_2 UNIQUE (email)
+);
+
+CREATE INDEX ix_users_email ON users (email ASC, password ASC);
+CREATE INDEX ix_users_username ON users (username ASC, password ASC);
+
+CREATE TRIGGER update_users_updated_at
+AFTER UPDATE ON users
+FOR EACH ROW
+BEGIN
+ UPDATE users SET updated_at = datetime('now') WHERE userid = NEW.userid;
+END;
+
+CREATE TABLE users_property
+(
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ name VARCHAR(20),
+ value VARCHAR(100),
+ userid INTEGER NOT NULL,
+
+ CONSTRAINT fk_custom_user FOREIGN KEY (userid) REFERENCES users (userid)
+);
+```
+
+
+
+
+PostgreSQL
+
+```sql
+CREATE TABLE users
+(
+ userid SERIAL NOT NULL,
+ name VARCHAR(50),
+ email VARCHAR(120),
+ username VARCHAR(15) NOT NULL,
+ password CHAR(40) NOT NULL,
+ created_at TIMESTAMP DEFAULT now(),
+ updated_at TIMESTAMP DEFAULT now(),
+ deleted_at TIMESTAMP,
+ role VARCHAR(20),
+
+ CONSTRAINT pk_users PRIMARY KEY (userid),
+ CONSTRAINT ix_username UNIQUE (username),
+ CONSTRAINT ix_users_2 UNIQUE (email)
+);
+
+CREATE INDEX ix_users_email ON users (email ASC, password ASC);
+CREATE INDEX ix_users_username ON users (username ASC, password ASC);
+
+CREATE OR REPLACE FUNCTION update_users_updated_at()
+RETURNS TRIGGER AS $$
+BEGIN
+ NEW.updated_at = now();
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER trigger_update_users_updated_at
+BEFORE UPDATE ON users
+FOR EACH ROW
+EXECUTE FUNCTION update_users_updated_at();
+
+CREATE TABLE users_property
+(
+ id SERIAL NOT NULL,
+ name VARCHAR(20),
+ value VARCHAR(100),
+ userid INTEGER NOT NULL,
+
+ CONSTRAINT pk_custom PRIMARY KEY (id),
+ CONSTRAINT fk_custom_user FOREIGN KEY (userid) REFERENCES users (userid)
+);
+```
+
+
+
+
+SQL Server
+
+```sql
+CREATE TABLE users
+(
+ userid INTEGER IDENTITY(1,1) NOT NULL,
+ name VARCHAR(50),
+ email VARCHAR(120),
+ username VARCHAR(15) NOT NULL,
+ password CHAR(40) NOT NULL,
+ created_at DATETIME DEFAULT GETDATE(),
+ updated_at DATETIME DEFAULT GETDATE(),
+ deleted_at DATETIME,
+ role VARCHAR(20),
+
+ CONSTRAINT pk_users PRIMARY KEY (userid),
+ CONSTRAINT ix_username UNIQUE (username),
+ CONSTRAINT ix_users_2 UNIQUE (email)
+);
+
+CREATE INDEX ix_users_email ON users (email ASC, password ASC);
+CREATE INDEX ix_users_username ON users (username ASC, password ASC);
+
+GO
+CREATE TRIGGER trigger_update_users_updated_at
+ON users
+AFTER UPDATE
+AS
+BEGIN
+ SET NOCOUNT ON;
+ UPDATE users
+ SET updated_at = GETDATE()
+ FROM users u
+ INNER JOIN inserted i ON u.userid = i.userid;
+END;
+GO
+
+CREATE TABLE users_property
+(
+ id INTEGER IDENTITY(1,1) NOT NULL,
+ name VARCHAR(20),
+ value VARCHAR(100),
+ userid INTEGER NOT NULL,
+
+ CONSTRAINT pk_custom PRIMARY KEY (id),
+ CONSTRAINT fk_custom_user FOREIGN KEY (userid) REFERENCES users (userid)
+);
+```
+
+
+
+## Basic Usage
+
+### Using Default Configuration
+
+```php
+addUser('John Doe', 'johndoe', 'john@example.com', 'password123');
+```
+
+## Architecture
+
+```text
+ ┌───────────────────┐
+ │ SessionContext │
+ └───────────────────┘
+ │
+ │
+ ┌───────────────────┐
+ │ UsersService │ (Business Logic)
+ └───────────────────┘
+ │
+ ┌────────────────────┴────────────────────┐
+ │ │
+ ┌───────────────────┐ ┌──────────────────────┐
+ │ UsersRepository │ │ PropertiesRepository │
+ └───────────────────┘ └──────────────────────┘
+ │ │
+ ┌───────┴───────┐ ┌──────────┴──────────┐
+ │ │ │ │
+ ┌───────────────┐ ┌────────┐ ┌───────────────┐ ┌──────────────┐
+ │ UserModel │ │ Mapper │ │ PropsModel │ │ Mapper │
+ └───────────────┘ └────────┘ └───────────────┘ └──────────────┘
+```
+
+- **UsersService**: High-level business logic for user operations
+- **UsersRepository**: Data access layer for user records
+- **UserPropertiesRepository**: Data access layer for user properties
+- **UserModel**: User entity with table/field mapping via attributes
+- **UserPropertiesModel**: Properties entity with table/field mapping
+- **Mapper**: Field transformation functions (e.g., PasswordSha1Mapper, ReadOnlyMapper)
+
+## 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..4ea2496
--- /dev/null
+++ b/docs/examples.md
@@ -0,0 +1,750 @@
+---
+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
+
+
+ = htmlspecialchars($error) ?>
+
+
+
+
+ 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
+
+
+ = htmlspecialchars($error) ?>
+
+
+
+
+ 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, = htmlspecialchars($user->getName()) ?>
+
+ Email: = htmlspecialchars($user->getEmail()) ?>
+ Logged in at: = date('Y-m-d H:i:s', $loginTime) ?>
+
+ hasRole('admin')): ?>
+ 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 {
+ $userToken = $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 ($userToken === null) {
+ jsonResponse(['error' => 'Invalid credentials'], 401);
+ }
+
+ jsonResponse([
+ 'success' => true,
+ 'token' => $userToken->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
+ $userToken = $users->isValidToken($username, $jwtWrapper, $token);
+
+ if ($userToken === null) {
+ jsonResponse(['error' => 'Token validation failed'], 401);
+ }
+
+ $user = $userToken->user;
+
+ // Handle request
+ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
+ // Get user info
+ jsonResponse([
+ 'id' => $user->getUserid(),
+ 'name' => $user->getName(),
+ 'email' => $user->getEmail(),
+ 'username' => $user->getUsername(),
+ 'role' => $user->getRole()
+ ]);
+ } 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');
+```
+
+## OAuth Authentication Example
+
+This example shows how to integrate OAuth authentication using `createInsecureAuthToken()`.
+
+```php
+ 'user@example.com',
+ 'name' => 'John Doe',
+ 'provider' => 'google',
+ 'provider_id' => '123456789'
+];
+
+try {
+ // Check if user exists
+ $user = $users->getByEmail($oauthUserData['email']);
+
+ if ($user === null) {
+ // Create new user for first-time OAuth login
+ $user = $users->addUser(
+ $oauthUserData['name'],
+ $oauthUserData['email'], // Use email as username
+ $oauthUserData['email'],
+ bin2hex(random_bytes(16)) // Random password (user won't use it)
+ );
+
+ // Store OAuth provider info
+ $users->addProperty($user->getUserid(), 'oauth_provider', $oauthUserData['provider']);
+ $users->addProperty($user->getUserid(), 'oauth_provider_id', $oauthUserData['provider_id']);
+ }
+
+ // Create token without password validation
+ $userToken = $users->createInsecureAuthToken(
+ $user, // Pass UserModel directly
+ $jwtWrapper,
+ 3600,
+ ['last_oauth_login' => date('Y-m-d H:i:s')],
+ [
+ 'auth_method' => 'oauth',
+ 'provider' => $oauthUserData['provider']
+ ]
+ );
+
+ // Return token to client
+ jsonResponse([
+ 'success' => true,
+ 'token' => $userToken->token,
+ 'user' => [
+ 'id' => $user->getUserid(),
+ 'name' => $user->getName(),
+ 'email' => $user->getEmail()
+ ]
+ ]);
+
+} catch (Exception $e) {
+ jsonResponse(['error' => $e->getMessage()], 500);
+}
+```
+
+## Token Refresh Example
+
+Implement a token refresh mechanism using `createInsecureAuthToken()`.
+
+```php
+ 'No refresh token provided'], 401);
+}
+
+$refreshToken = $matches[1];
+
+try {
+ // Decode refresh token
+ $jwtData = $refreshWrapper->extractData($refreshToken);
+ $userId = $jwtData->data['userid'] ?? null;
+
+ if (!$userId) {
+ jsonResponse(['error' => 'Invalid refresh token'], 401);
+ }
+
+ // Get user
+ $user = $users->getById($userId);
+
+ if ($user === null) {
+ jsonResponse(['error' => 'User not found'], 404);
+ }
+
+ // Verify refresh token is valid
+ $userToken = $users->isValidToken($user->getEmail(), $refreshWrapper, $refreshToken);
+
+ if ($userToken === null) {
+ jsonResponse(['error' => 'Invalid or expired refresh token'], 401);
+ }
+
+ // Create new access token (using main JWT wrapper)
+ $newAccessToken = $users->createInsecureAuthToken(
+ $user,
+ $jwtWrapper,
+ 900, // 15 minutes
+ [],
+ ['token_type' => 'access']
+ );
+
+ jsonResponse([
+ 'success' => true,
+ 'access_token' => $newAccessToken->token,
+ 'expires_in' => 900
+ ]);
+
+} catch (Exception $e) {
+ jsonResponse(['error' => $e->getMessage()], 500);
+}
+```
+
+### Initial Login with Refresh Token
+
+```php
+ 'Method not allowed'], 405);
+}
+
+$input = json_decode(file_get_contents('php://input'), true);
+$username = $input['username'] ?? '';
+$password = $input['password'] ?? '';
+
+try {
+ // Main JWT wrapper for access tokens
+ $jwtSecret = getenv('JWT_SECRET') ?: 'secret-here==';
+ $jwtWrapper = new JwtWrapper('api.example.com', new JwtHashHmacSecret($jwtSecret));
+
+ // Separate wrapper for refresh tokens
+ $refreshSecret = getenv('JWT_REFRESH_SECRET') ?: 'refresh-secret-here==';
+ $refreshWrapper = new JwtWrapper('api.example.com', new JwtHashHmacSecret($refreshSecret));
+
+ // Validate credentials
+ $user = $users->isValidUser($username, $password);
+
+ if ($user === null) {
+ jsonResponse(['error' => 'Invalid credentials'], 401);
+ }
+
+ // Create short-lived access token
+ $accessToken = $users->createInsecureAuthToken(
+ $user,
+ $jwtWrapper,
+ 900, // 15 minutes
+ ['last_login' => date('Y-m-d H:i:s')],
+ ['token_type' => 'access']
+ );
+
+ // Create long-lived refresh token
+ $refreshToken = $users->createInsecureAuthToken(
+ $user,
+ $refreshWrapper,
+ 604800, // 7 days
+ [],
+ ['token_type' => 'refresh']
+ );
+
+ jsonResponse([
+ 'success' => true,
+ 'access_token' => $accessToken->token,
+ 'refresh_token' => $refreshToken->token,
+ 'expires_in' => 900
+ ]);
+
+} catch (Exception $e) {
+ jsonResponse(['error' => $e->getMessage()], 500);
+}
+```
+
+## 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..540ee4e
--- /dev/null
+++ b/docs/getting-started.md
@@ -0,0 +1,50 @@
+---
+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 a clean repository and service layer architecture for managing users, authentication, and user properties with database storage.
+
+## Quick Example
+
+```php
+addUser('John Doe', 'johndoe', 'john@example.com', 'SecurePass123');
+$user = $users->isValidUser('johndoe', 'SecurePass123');
+
+if ($user !== null) {
+ $sessionContext = new SessionContext(Factory::createSessionPool());
+ $sessionContext->registerLogin($user->getUserid());
+ echo "User authenticated successfully!";
+}
+```
+
+To authenticate users by email instead of username, create the service with `LoginField::Email`.
+
+## 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..6b3c7f9
--- /dev/null
+++ b/docs/installation.md
@@ -0,0 +1,32 @@
+---
+sidebar_position: 2
+title: Installation
+---
+
+# Installation
+
+## Requirements
+
+- PHP 8.2 or higher
+- Composer
+
+## Install via Composer
+
+Install the library using Composer:
+
+```bash
+composer require byjg/authuser
+```
+
+## 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..22cd7f5
--- /dev/null
+++ b/docs/jwt-tokens.md
@@ -0,0 +1,462 @@
+---
+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 ($userToken !== null) {
+ // Return token to client
+ echo json_encode(['token' => $userToken->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'
+ ]
+);
+
+// Access the token string
+$token = $userToken->token;
+```
+
+### 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'
+ ]
+);
+```
+
+### Copy User Fields Automatically
+
+Instead of manually adding every field to `$updateTokenInfo`, pass a seventh argument with an array of `User` enum values or string property names. The service will read the corresponding getters (or custom properties) from the `UserModel` and copy them into the token payload.
+
+```php
+createAuthToken(
+ 'johndoe',
+ 'password123',
+ $jwtWrapper,
+ 3600,
+ [],
+ [],
+ [UserField::Name, UserField::Email, 'department']
+);
+```
+
+In the example above, the token payload receives the user's `name`, `email`, and the value returned by `$user->get('department')` automatically.
+
+### Creating Tokens Without Password Validation
+
+The `createInsecureAuthToken()` method creates JWT tokens without validating the user's password. This is useful for:
+- Creating tokens after social authentication (OAuth, SAML, etc.)
+- Implementing "remember me" functionality
+- Token refresh mechanisms
+- Administrative token generation
+
+:::warning Security Warning
+Use `createInsecureAuthToken()` with caution. Only call it after you've verified the user's identity through another secure method.
+:::
+
+#### Using Login String
+
+```php
+createInsecureAuthToken(
+ 'johndoe', // Login (username or email)
+ $jwtWrapper,
+ 3600, // Expires in 1 hour
+ [], // Update user properties (optional)
+ ['auth_method' => 'oauth'] // Additional token data
+);
+
+echo "Token: " . $userToken->token;
+```
+
+#### Using UserModel Object
+
+```php
+getByEmail($oauthEmail);
+
+if ($user !== null) {
+ $userToken = $users->createInsecureAuthToken(
+ $user, // Pass UserModel directly
+ $jwtWrapper,
+ 3600,
+ ['last_oauth_login' => date('Y-m-d H:i:s')],
+ ['oauth_provider' => 'google']
+ );
+
+ echo "Token: " . $userToken->token;
+}
+```
+
+#### Comparison: createAuthToken vs createInsecureAuthToken
+
+| Feature | createAuthToken | createInsecureAuthToken |
+|---------------------|-------------------|----------------------------------------|
+| Password validation | ✅ Required | ❌ Skipped |
+| First parameter | Login string only | Login string OR UserModel |
+| Use case | Normal login | OAuth, token refresh, admin operations |
+| Security level | High | Use with caution |
+
+## Validating JWT Tokens
+
+### Token Validation
+
+```php
+isValidToken('johndoe', $jwtWrapper, $token);
+
+ if ($userToken !== null) {
+ $user = $userToken->user; // UserModel instance
+ $tokenData = $userToken->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 ($userToken === null) {
+ throw new Exception('Authentication failed');
+ }
+
+ echo json_encode([
+ 'success' => true,
+ 'token' => $userToken->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
+ $userToken = $users->isValidToken($username, $jwtWrapper, $token);
+
+ if ($userToken === null) {
+ throw new Exception('Invalid token');
+ }
+
+ $user = $userToken->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->getByLogin($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
+$refreshUserToken = $users->createAuthToken(
+ $login,
+ $password,
+ $jwtWrapperRefresh, // Different wrapper/key
+ 604800, // 7 days
+ [],
+ ['type' => 'refresh']
+);
+
+echo json_encode([
+ 'access_token' => $accessUserToken->token,
+ 'refresh_token' => $refreshUserToken->token
+]);
+```
+
+## 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..f881e3f
--- /dev/null
+++ b/docs/mappers.md
@@ -0,0 +1,269 @@
+---
+sidebar_position: 11
+title: Mappers and Entity Processors
+---
+
+# Mappers and Entity Processors
+
+Auth User PHP relies on [byjg/micro-orm](https://github.com/byjg/micro-orm) to map models to database rows. Every property in `UserModel` and `UserPropertiesModel` can define how its value is transformed when it is inserted, updated, or selected. This page shows how to take advantage of those hooks.
+
+## Controlling Fields with Attributes
+
+`FieldAttribute` accepts optional mapper functions for each lifecycle event:
+
+- `updateFunction` – runs before an UPDATE or when calling `save()` (most common).
+- `insertFunction` – runs only when the record is first inserted.
+- `selectFunction` – runs when values are loaded from the database.
+
+```php
+ strtolower(trim((string) $value))),
+ selectFunction: StandardMapper::class
+ )]
+ protected ?string $email = null;
+}
+```
+
+### Built-in Mapper Helpers
+
+This package ships with a few mapper utilities that complement the ones provided by Micro ORM:
+
+- **PasswordSha1Mapper** – hashes passwords using SHA-1 (the default on `UserModel`). Replace it with your own mapper to change the hashing algorithm.
+- **UserIdGeneratorMapper** – derives a user ID from the username when the primary key is empty.
+ ```php
+ 12]);
+ }
+}
+```
+
+Attach it to the password field:
+
+```php
+getUsername();
+ if (strlen($username) < 3) {
+ throw new \InvalidArgumentException('Username must have at least 3 characters.');
+ }
+
+ if (!preg_match('/^[A-Za-z0-9_]+$/', $username)) {
+ throw new \InvalidArgumentException('Username can only contain letters, numbers, and underscores.');
+ }
+ }
+}
+
+class AuditProcessor implements EntityProcessorInterface
+{
+ public function __construct(private readonly int $actorId)
+ {
+ }
+
+ public function process(mixed $instance): void
+ {
+ if ($instance instanceof UserModel) {
+ $instance->set('modified_by', (string) $this->actorId);
+ $instance->set('modified_at', date('Y-m-d H:i:s'));
+ }
+ }
+}
+```
+
+Attach them using the table attribute:
+
+```php
+getUsersRepository()->getRepository();
+$repository->setBeforeUpdate(new AuditProcessor($currentUserId));
+```
+
+## Complete Example
+
+```php
+ strtolower(trim((string) $value))))]
+ protected ?string $email = null;
+}
+
+$dbDriver = DbFactory::getDbInstance('mysql://user:pass@localhost/app');
+$db = DatabaseExecutor::using($dbDriver);
+$usersRepo = new UsersRepository($db, CustomUserModel::class);
+$propsRepo = new UserPropertiesRepository($db, \ByJG\Authenticate\Model\UserPropertiesModel::class);
+$users = new UsersService($usersRepo, $propsRepo, LoginField::Username);
+```
+
+With these tools you can precisely control how data flows between your database schema and the authentication service while keeping the domain models clean.
diff --git a/docs/password-validation.md b/docs/password-validation.md
new file mode 100644
index 0000000..3082d2f
--- /dev/null
+++ b/docs/password-validation.md
@@ -0,0 +1,327 @@
+---
+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
+```
+
+### Applying Rules Through UsersService
+
+Instead of attaching the definition to every `UserModel` manually, pass it to the `UsersService` constructor. Every entity created through the service (e.g., via `addUser()` or `getUsersEntity()`) receives the same `PasswordDefinition`.
+
+```php
+ 12,
+ PasswordDefinition::REQUIRE_NUMBERS => 2,
+]);
+
+$users = new UsersService(
+ $usersRepo,
+ $propsRepo,
+ LoginField::Username,
+ $passwordDefinition
+);
+
+$newUser = $users->addUser('Jane', 'jane', 'jane@example.com', 'StrongPassword123!');
+```
+
+Need a blank entity with the validation rules already loaded? Use `getUsersEntity()`:
+
+```php
+getUsersEntity([
+ 'name' => 'John Doe',
+ 'email' => 'john@example.com',
+]);
+$user->setPassword('AnotherStrongPass123!');
+$users->save($user);
+```
+
+This approach guarantees that every model created or retrieved through the service (`getById()`, `getByEmail()`, `getUsersByProperty()`, etc.) automatically enforces your password policy.
+
+## 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..697c4fe
--- /dev/null
+++ b/docs/session-context.md
@@ -0,0 +1,208 @@
+---
+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);
+```
+Call `setSessionData()` after `registerLogin()` if you need to store extra session metadata (e.g., IP address, login time).
+
+### 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..c100d4e
--- /dev/null
+++ b/docs/user-management.md
@@ -0,0 +1,190 @@
+---
+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->setRole('user');
+
+$savedUser = $users->save($userModel);
+```
+
+### Creating Entities via UsersService
+
+When you need a new `UserModel` that already respects the mapper configuration (including any `PasswordDefinition` passed to the `UsersService` constructor), call `getUsersEntity()`:
+
+```php
+getUsersEntity([
+ 'name' => 'Jane Smith',
+ 'email' => 'jane@example.com',
+ 'username' => 'janesmith',
+]);
+$user->setPassword('Str0ngPass!');
+$users->save($user);
+```
+
+The returned model is identical to instantiating `UserModel` manually, but it is pre-configured with the service-level password rules and field mappings.
+
+## Retrieving Users
+
+The `UsersService` provides several methods to retrieve 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 `UsersService` constructor (either email or username):
+
+```php
+getByLogin('johndoe');
+```
+
+### Using Custom Queries
+
+For advanced queries, use the repository directly:
+
+```php
+getUsersRepository();
+
+// Build custom query
+$query = Query::getInstance()
+ ->table('users')
+ ->where('email = :email', ['email' => 'john@example.com'])
+ ->where('admin = :admin', ['admin' => 'yes']);
+
+$results = $usersRepo->getRepository()->getByQuery($query);
+```
+
+## 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
+removeById($userId);
+```
+
+### Delete by Login
+
+```php
+removeByLogin('johndoe');
+```
+
+## Checking User Roles
+
+Users can have assigned roles stored in the `role` field. Use `$user->hasRole()` to check if a user has a specific role:
+
+```php
+hasRole('admin')) {
+ echo "User is an administrator";
+}
+
+if ($user->hasRole('moderator')) {
+ echo "User is a moderator";
+}
+
+// Set a role
+$user->setRole('admin');
+$users->save($user);
+
+// Get current role
+$role = $user->getRole();
+```
+
+The `hasRole()` method performs case-insensitive comparison, so `hasRole('admin')` and `hasRole('ADMIN')` are equivalent.
+
+## UserModel Properties
+
+The `UserModel` class provides the following properties:
+
+| Property | Type | Description |
+|-------------|-----------------------------------|------------------------------------------------------------------|
+| `userid` | string\|int\|Literal\|null | Primary key. Generated by the database unless you override it. |
+| `name` | string\|null | User's display name. |
+| `email` | string\|null | User's email address. |
+| `username` | string\|null | Unique username (used when `LoginField::Username` is selected). |
+| `password` | string\|null | Hashed password. Uses `PasswordSha1Mapper` by default. |
+| `role` | string\|null | Optional application role. |
+| `createdAt` | string\|null | Automatically set when the record is inserted. |
+| `updatedAt` | string\|null | Automatically updated whenever the record is saved. |
+| `deletedAt` | string\|null | Nullable soft-delete timestamp (requires manual handling). |
+| `properties`| `UserPropertiesModel[]` (array) | Loaded via `getProperties()` or manipulated through helpers. |
+
+## 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..e721387
--- /dev/null
+++ b/docs/user-properties.md
@@ -0,0 +1,244 @@
+---
+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";
+}
+```
+
+## 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..e1657e9 100644
--- a/example.php
+++ b/example.php
@@ -2,20 +2,51 @@
require "vendor/autoload.php";
-$users = new ByJG\Authenticate\UsersAnyDataset('/tmp/pass.anydata.xml');
+use ByJG\AnyDataset\Db\DatabaseExecutor;
+use ByJG\AnyDataset\Db\Factory as DbFactory;
+use ByJG\Authenticate\Model\UserModel;
+use ByJG\Authenticate\Model\UserPropertiesModel;
+use ByJG\Authenticate\Repository\UserPropertiesRepository;
+use ByJG\Authenticate\Repository\UsersRepository;
+use ByJG\Authenticate\Service\UsersService;
+use ByJG\Authenticate\SessionContext;
+use ByJG\Cache\Factory;
-$users->addUser('Some User Full Name', 'someuser', 'someuser@someemail.com', '12345');
-//$users->save();
+// Create database connection (using SQLite for this example)
+$dbDriver = DbFactory::getDbInstance('sqlite:///tmp/users.db');
+$db = DatabaseExecutor::using($dbDriver);
-$user = $users->isValidUser('someuser', '12345');
-var_dump($user);
-if (!is_null($user))
-{
- $session = new \ByJG\Authenticate\SessionContext();
- $session->registerLogin($userId);
+// Initialize repositories
+$usersRepository = new UsersRepository($db, UserModel::class);
+$propertiesRepository = new UserPropertiesRepository($db, UserPropertiesModel::class);
- echo "Authenticated: " . $session->isAuthenticated();
- print_r($session->userInfo());
+// Initialize user service
+$users = new UsersService($usersRepository, $propertiesRepository, UsersService::LOGIN_IS_USERNAME);
+
+// 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";
}
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 254ada2..90e6448 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -6,15 +6,21 @@ and open the template in the editor.
-->
-
+ displayDetailsOnTestsThatTriggerDeprecations="true"
+ displayDetailsOnTestsThatTriggerErrors="true"
+ displayDetailsOnTestsThatTriggerNotices="true"
+ displayDetailsOnTestsThatTriggerWarnings="true"
+ displayDetailsOnPhpunitDeprecations="true"
+ failOnWarning="true"
+ failOnNotice="true"
+ failOnDeprecation="true"
+ failOnPhpunitDeprecation="true"
+ stopOnFailure="false"
+ xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd">
@@ -22,11 +28,11 @@ and open the template in the editor.
-
-
- ./src
-
-
+
+
+ ./src/
+
+
diff --git a/psalm.xml b/psalm.xml
index ebabb1a..b208114 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -4,6 +4,7 @@
resolveFromConfigFile="true"
findUnusedBaselineEntry="true"
findUnusedCode="false"
+ cacheDirectory="/tmp/psalm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
diff --git a/src/Definition/PasswordDefinition.php b/src/Definition/PasswordDefinition.php
index e4e1e6b..5697a47 100644
--- a/src/Definition/PasswordDefinition.php
+++ b/src/Definition/PasswordDefinition.php
@@ -2,7 +2,9 @@
namespace ByJG\Authenticate\Definition;
+use ByJG\Authenticate\Interfaces\PasswordMapperInterface;
use InvalidArgumentException;
+use Random\RandomException;
class PasswordDefinition
{
@@ -17,6 +19,8 @@ class PasswordDefinition
protected array $rules = [];
+ protected PasswordMapperInterface|string|null $passwordMapper = null;
+
public function __construct($rules = null)
{
$this->rules = [
@@ -47,7 +51,7 @@ public function getRules(): array
return $this->rules;
}
- public function getRule($rule): string|bool|int
+ public function getRule(string $rule): string|bool|int
{
if (!array_key_exists($rule, $this->rules)) {
throw new InvalidArgumentException("Invalid rule");
@@ -113,6 +117,95 @@ public function matchPassword(string $password): int
return $result;
}
+ /**
+ * @throws RandomException
+ */
+ public function generatePassword(int $extendSize = 0): string
+ {
+ $charsList = [
+ self::REQUIRE_UPPERCASE => 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ self::REQUIRE_LOWERCASE => 'abcdefghijklmnopqrstuvwxyz',
+ self::REQUIRE_SYMBOLS => '!@#$%^&*()-_=+{};:,<.>',
+ self::REQUIRE_NUMBERS => '0123456789'
+ ];
+
+ $charsCount = [
+ self::REQUIRE_UPPERCASE => 0,
+ self::REQUIRE_LOWERCASE => 0,
+ self::REQUIRE_SYMBOLS => 0,
+ self::REQUIRE_NUMBERS => 0
+ ];
+
+ foreach ($this->rules as $rule => $value) {
+ if ($rule == self::MINIMUM_CHARS) {
+ continue;
+ }
+
+ switch ($rule) {
+ case self::REQUIRE_UPPERCASE:
+ $charsCount[self::REQUIRE_UPPERCASE] = $value;
+ break;
+ case self::REQUIRE_LOWERCASE:
+ $charsCount[self::REQUIRE_LOWERCASE] = $value;
+ break;
+ case self::REQUIRE_SYMBOLS:
+ $charsCount[self::REQUIRE_SYMBOLS] = $value;
+ break;
+ case self::REQUIRE_NUMBERS:
+ $charsCount[self::REQUIRE_NUMBERS] = $value;
+ break;
+ }
+ }
+
+ $size = intval($this->rules[self::MINIMUM_CHARS]) + $extendSize;
+ $totalChars = intval(array_sum($charsCount));
+ $rulesWithValueGreaterThanZero = array_filter($charsCount, function ($value) {
+ return $value > 0;
+ });
+ if (empty($rulesWithValueGreaterThanZero)) {
+ $rulesWithValueGreaterThanZero[self::REQUIRE_LOWERCASE] = 1;
+ $rulesWithValueGreaterThanZero[self::REQUIRE_NUMBERS] = 1;
+ }
+ while ($totalChars < $size) {
+ $rule = array_rand($rulesWithValueGreaterThanZero);
+ $rulesWithValueGreaterThanZero[$rule]++;
+ $totalChars++;
+ }
+
+ $password = '';
+ while (strlen($password) < $size) {
+ foreach ($rulesWithValueGreaterThanZero as $rule => $value) {
+ if ($value == 0) {
+ continue;
+ }
+
+ do {
+ $char = $charsList[$rule][random_int(0, strlen($charsList[$rule]) - 1)];
+ $previousChar = $password[strlen($password) - 1] ?? "\0";
+ $isRepeated = ($char == $previousChar);
+ $previousChar = strtoupper($previousChar);
+ $upperChar = strtoupper($char);
+ $isSequential = ($upperChar == chr(ord($previousChar) + 1)) || ($upperChar == chr(ord($previousChar) - 1));
+ if (!$isRepeated && !$isSequential) {
+ break;
+ }
+ } while (true);
+ $password .= $char;
+ $rulesWithValueGreaterThanZero[$rule]--;
+ }
+ }
+ return $password;
+ }
+ public function getPasswordMapper(): PasswordMapperInterface|string|null
+ {
+ return $this->passwordMapper;
+ }
+
+ // This is used internally;
+ public function setPasswordMapper(PasswordMapperInterface|string|null $passwordMapper): void
+ {
+ $this->passwordMapper = $passwordMapper;
+ }
}
\ No newline at end of file
diff --git a/src/Definition/UserDefinition.php b/src/Definition/UserDefinition.php
deleted file mode 100644
index 1019c98..0000000
--- a/src/Definition/UserDefinition.php
+++ /dev/null
@@ -1,262 +0,0 @@
- [], "update" => [] ];
- protected string $__loginField;
- protected string $__model;
- protected array $__properties = [];
- protected Closure|null $__generateKey = null;
-
- const FIELD_USERID = 'userid';
- const FIELD_NAME = 'name';
- const FIELD_EMAIL = 'email';
- const FIELD_USERNAME = 'username';
- const FIELD_PASSWORD = 'password';
- const FIELD_CREATED = 'created';
- const FIELD_ADMIN = 'admin';
-
- const UPDATE="update";
- const SELECT="select";
-
-
- const LOGIN_IS_EMAIL="email";
- const LOGIN_IS_USERNAME="username";
-
- /**
- * Define the name of fields and table to store and retrieve info from database
- *
- * @param string $table
- * @param string $model
- * @param string $loginField
- * @param array $fieldDef
- */
- public function __construct(
- string $table = 'users',
- string $model = UserModel::class,
- string $loginField = self::LOGIN_IS_USERNAME,
- array $fieldDef = []
- ) {
- $this->__table = $table;
- $this->__model = $model;
-
- // Set Default User Definition
- $modelInstance = $this->modelInstance();
- $modelProperties = Serialize::from($modelInstance)->toArray();
- foreach (array_keys($modelProperties) as $property) {
- $this->__properties[$property] = $property;
- }
-
- // Set custom Properties
- foreach ($fieldDef as $property => $value) {
- $this->checkProperty($property);
- $this->__properties[$property] = $value;
- }
-
- $this->defineClosureForUpdate(UserDefinition::FIELD_PASSWORD, function ($value) {
- // Already have a SHA1 password
- if (strlen($value) === 40) {
- return $value;
- }
-
- // Leave null
- if (empty($value)) {
- return null;
- }
-
- // Return the hash password
- return strtolower(sha1($value));
- });
-
- if ($loginField !== self::LOGIN_IS_USERNAME && $loginField !== self::LOGIN_IS_EMAIL) {
- throw new InvalidArgumentException('Login field is invalid. ');
- }
- $this->__loginField = $loginField;
-
- $this->beforeInsert = function ($instance) {
- return $instance;
- };
- $this->beforeUpdate = function ($instance) {
- return $instance;
- };
- }
-
- /**
- * @return string
- */
- public function table(): string
- {
- return $this->__table;
- }
-
-
- public function __get(string $name)
- {
- $this->checkProperty($name);
- return $this->__properties[$name];
- }
-
- public function __call(string $name, array $arguments)
- {
- if (str_starts_with($name, 'get')) {
- $name = strtolower(substr($name, 3));
- return $this->{$name};
- }
- throw new InvalidArgumentException("Method '$name' does not exists'");
- }
-
- public function toArray(): array
- {
- return $this->__properties;
- }
-
- /**
- * @return string
- */
- public function loginField(): string
- {
- return $this->{$this->__loginField};
- }
-
- private function checkProperty(string $property): void
- {
- if (!isset($this->__properties[$property])) {
- throw new InvalidArgumentException("Property '$property' does not exists'");
- }
- }
-
- /**
- * @param string $event
- * @param string $property
- * @param Closure $closure
- */
- private function updateClosureDef(string $event, string $property, Closure $closure): void
- {
- $this->checkProperty($property);
- $this->__closures[$event][$property] = $closure;
- }
-
- private function getClosureDef(string $event, string $property): Closure
- {
- $this->checkProperty($property);
-
- if (!$this->existsClosure($event, $property)) {
- return MapperClosure::standard();
- }
-
- return $this->__closures[$event][$property];
- }
-
- public function existsClosure(string $event, string $property): bool
- {
- // Event not set
- if (!isset($this->__closures[$event])) {
- return false;
- }
-
- // Event is set but there is no property
- if (!array_key_exists($property, $this->__closures[$event])) {
- return false;
- }
-
- return true;
- }
-
- public function markPropertyAsReadOnly(string $property): void
- {
- $this->updateClosureDef(self::UPDATE, $property, MapperClosure::readOnly());
- }
-
- public function defineClosureForUpdate(string $property, Closure $closure): void
- {
- $this->updateClosureDef(self::UPDATE, $property, $closure);
- }
-
- public function defineClosureForSelect(string $property, Closure $closure): void
- {
- $this->updateClosureDef(self::SELECT, $property, $closure);
- }
-
- public function getClosureForUpdate(string $property): Closure
- {
- return $this->getClosureDef(self::UPDATE, $property);
- }
-
- public function defineGenerateKeyClosure(Closure $closure): void
- {
- $this->__generateKey = $closure;
- }
-
- public function getGenerateKeyClosure(): ?Closure
- {
- return $this->__generateKey;
- }
-
- /**
- * @param $property
- * @return Closure
- */
- public function getClosureForSelect($property): Closure
- {
- return $this->getClosureDef(self::SELECT, $property);
- }
-
- public function model(): string
- {
- return $this->__model;
- }
-
- public function modelInstance(): UserModel
- {
- $model = $this->__model;
- return new $model();
- }
-
- protected Closure $beforeInsert;
-
- /**
- * @return Closure
- */
- public function getBeforeInsert(): Closure
- {
- return $this->beforeInsert;
- }
-
- /**
- * @param Closure $beforeInsert
- */
- public function setBeforeInsert(Closure $beforeInsert): void
- {
- $this->beforeInsert = $beforeInsert;
- }
-
- protected Closure $beforeUpdate;
-
- /**
- * @return Closure
- */
- public function getBeforeUpdate(): Closure
- {
- return $this->beforeUpdate;
- }
-
- /**
- * @param mixed $beforeUpdate
- */
- public function setBeforeUpdate(Closure $beforeUpdate): void
- {
- $this->beforeUpdate = $beforeUpdate;
- }
-}
diff --git a/src/Definition/UserPropertiesDefinition.php b/src/Definition/UserPropertiesDefinition.php
deleted file mode 100644
index 9ee9edc..0000000
--- a/src/Definition/UserPropertiesDefinition.php
+++ /dev/null
@@ -1,76 +0,0 @@
-_UserTable->Id = "userid"].
- *
- * @param string $table
- * @param string $id
- * @param string $name
- * @param string $value
- * @param string $userid
- */
- public function __construct(
- string $table = 'users_property',
- string $id = 'id',
- string $name = 'name',
- string $value = 'value',
- string $userid = UserDefinition::FIELD_USERID
- ) {
- $this->table = $table;
- $this->id = $id;
- $this->name = $name;
- $this->value = $value;
- $this->userid = $userid;
- }
-
- /**
- * @return string
- */
- public function table(): string
- {
- return $this->table;
- }
-
- /**
- * @return string
- */
- public function getId(): string
- {
- return $this->id;
- }
-
- /**
- * @return string
- */
- public function getName(): string
- {
- return $this->name;
- }
-
- /**
- * @return string
- */
- public function getValue(): string
- {
- return $this->value;
- }
-
- /**
- * @return string
- */
- public function getUserid(): string
- {
- return $this->userid;
- }
-}
diff --git a/src/Enum/LoginField.php b/src/Enum/LoginField.php
new file mode 100644
index 0000000..79ae8ef
--- /dev/null
+++ b/src/Enum/LoginField.php
@@ -0,0 +1,9 @@
+|null|string String vector with all sites
- */
- public function getProperty(string|HexUuidLiteral|int $userId, string $propertyName): array|string|\ByJG\Authenticate\Model\UserPropertiesModel|null;
-
- /**
- *
- * @param string|int|HexUuidLiteral $userId
- * @param string $propertyName
- * @param string|null $value
- */
- public function addProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value): bool;
-
- /**
- *
- * @param string|int|HexUuidLiteral $userId
- * @param string $propertyName
- * @param string|null $value
- */
- public function setProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value): bool;
-
- /**
- *
- * @param string|int|HexUuidLiteral $userId
- * @param string $propertyName
- * @param string|null $value
- */
- public function removeProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value = null): bool;
-
- /**
- * @desc Remove a specific site from all users
- * @param string $propertyName
- * @param string|null $value
- * @return void
- */
- public function removeAllProperties(string $propertyName, string|null $value = null): void;
-
- /**
- * Authenticate a user and create a token if it is valid
- *
- * @param string $login
- * @param string $password
- * @param JwtWrapper $jwtWrapper
- * @param int $expires
- * @param array $updateUserInfo
- * @param array $updateTokenInfo
- * @return string|null Return the TOKEN or null, if can't create it.
- */
- public function createAuthToken(
- string $login,
- string $password,
- JwtWrapper $jwtWrapper,
- int $expires = 1200,
- array $updateUserInfo = [],
- array $updateTokenInfo = []
- ): string|null;
-
- /**
- * Check if the Auth Token is valid
- *
- * @param string $login
- * @param JwtWrapper $jwtWrapper
- * @param string $token
- * @return array|null
- */
- public function isValidToken(string $login, JwtWrapper $jwtWrapper, string $token): array|null;
-
- /**
- * @return UserDefinition Description
- */
- public function getUserDefinition(): UserDefinition;
-
- /**
- * @return UserPropertiesDefinition Description
- */
- public function getUserPropertiesDefinition(): UserPropertiesDefinition;
-
- /**
- * @param string|int|HexUuidLiteral $userid
- * @return bool
- */
- public function removeUserById(string|HexUuidLiteral|int $userid): bool;
-}
diff --git a/src/Interfaces/UsersServiceInterface.php b/src/Interfaces/UsersServiceInterface.php
new file mode 100644
index 0000000..6ee739f
--- /dev/null
+++ b/src/Interfaces/UsersServiceInterface.php
@@ -0,0 +1,210 @@
+closure = $closure;
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ #[\Override]
+ public function processedValue(mixed $value, mixed $instance, mixed $executor = null): mixed
+ {
+ $reflection = new ReflectionFunction($this->closure);
+ $paramCount = $reflection->getNumberOfParameters();
+
+ // Call closure with the appropriate number of parameters
+ return match($paramCount) {
+ 1 => ($this->closure)($value),
+ 2 => ($this->closure)($value, $instance),
+ default => ($this->closure)($value, $instance, $executor)
+ };
+ }
+}
diff --git a/src/MapperFunctions/PasswordSha1Mapper.php b/src/MapperFunctions/PasswordSha1Mapper.php
new file mode 100644
index 0000000..db3b79d
--- /dev/null
+++ b/src/MapperFunctions/PasswordSha1Mapper.php
@@ -0,0 +1,35 @@
+isPasswordEncrypted($value)) {
+ return $value;
+ }
+
+ // Leave null
+ if (empty($value)) {
+ return null;
+ }
+
+ // Return the hash password
+ return strtolower(sha1($value));
+ }
+
+ #[\Override]
+ public function isPasswordEncrypted(mixed $password): bool
+ {
+ return (is_string($password) && strlen($password) === 40);
+ }
+}
diff --git a/src/MapperFunctions/UserIdGeneratorMapper.php b/src/MapperFunctions/UserIdGeneratorMapper.php
new file mode 100644
index 0000000..40ec5a0
--- /dev/null
+++ b/src/MapperFunctions/UserIdGeneratorMapper.php
@@ -0,0 +1,29 @@
+getUsername()));
+ }
+
+ return $value;
+ }
+}
diff --git a/src/Model/UserModel.php b/src/Model/UserModel.php
index 3dd4ee3..e9e5c81 100644
--- a/src/Model/UserModel.php
+++ b/src/Model/UserModel.php
@@ -3,18 +3,38 @@
namespace ByJG\Authenticate\Model;
use ByJG\Authenticate\Definition\PasswordDefinition;
-use ByJG\MicroOrm\Literal\HexUuidLiteral;
+use ByJG\Authenticate\MapperFunctions\PasswordSha1Mapper;
+use ByJG\MicroOrm\Attributes\FieldAttribute;
+use ByJG\MicroOrm\Attributes\TableAttribute;
+use ByJG\MicroOrm\Literal\Literal;
+use ByJG\MicroOrm\Trait\CreatedAt;
+use ByJG\MicroOrm\Trait\DeletedAt;
+use ByJG\MicroOrm\Trait\UpdatedAt;
use InvalidArgumentException;
+#[TableAttribute(tableName: 'users')]
class UserModel
{
- protected string|int|HexUuidLiteral|null $userid = null;
+ use CreatedAt;
+ use UpdatedAt;
+ use DeletedAt;
+ #[FieldAttribute(primaryKey: true)]
+ protected string|int|Literal|null $userid = null;
+
+ #[FieldAttribute]
protected ?string $name = null;
+
+ #[FieldAttribute]
protected ?string $email = null;
+
+ #[FieldAttribute]
protected ?string $username = null;
+
+ #[FieldAttribute(updateFunction: PasswordSha1Mapper::class)]
protected ?string $password = null;
- protected ?string $created = null;
- protected ?string $admin = null;
+
+ #[FieldAttribute]
+ protected ?string $role = null;
protected ?PasswordDefinition $passwordDefinition = null;
@@ -27,30 +47,30 @@ class UserModel
* @param string $email
* @param string $username
* @param string $password
- * @param string $admin
+ * @param string $role
*/
- public function __construct(string $name = "", string $email = "", string $username = "", string $password = "", string $admin = "no")
+ public function __construct(string $name = "", string $email = "", string $username = "", string $password = "", string $role = "")
{
$this->name = $name;
$this->email = $email;
$this->username = $username;
$this->setPassword($password);
- $this->admin = $admin;
+ $this->role = $role;
}
/**
- * @return string|int|HexUuidLiteral|null
+ * @return string|int|Literal|null
*/
- public function getUserid(): string|int|HexUuidLiteral|null
+ public function getUserid(): string|int|Literal|null
{
return $this->userid;
}
/**
- * @param string|int|HexUuidLiteral|null $userid
+ * @param string|int|Literal|null $userid
*/
- public function setUserid(string|int|HexUuidLiteral|null $userid): void
+ public function setUserid(string|int|Literal|null $userid): void
{
$this->userid = $userid;
}
@@ -120,7 +140,7 @@ public function setPassword(?string $password): void
if (!empty($this->passwordDefinition) && !empty($password) && strlen($password) != 40) {
$match = $this->passwordDefinition->matchPassword($password);
if ($match != PasswordDefinition::SUCCESS) {
- throw new InvalidArgumentException("Password does not match the password definition [{$match}]");
+ throw new InvalidArgumentException("Password does not match the password definition [$match]");
}
}
@@ -130,33 +150,28 @@ public function setPassword(?string $password): void
/**
* @return string|null
*/
- public function getCreated(): ?string
- {
- return $this->created;
- }
-
- /**
- * @param string|null $created
- */
- public function setCreated(?string $created): void
+ public function getRole(): ?string
{
- $this->created = $created;
+ return $this->role;
}
/**
- * @return string|null
+ * @param string|null $role
*/
- public function getAdmin(): ?string
+ public function setRole(?string $role): void
{
- return $this->admin;
+ $this->role = $role;
}
/**
- * @param string|null $admin
+ * Check if user has a specific role
+ *
+ * @param string $role Role name to check
+ * @return bool
*/
- public function setAdmin(?string $admin): void
+ public function hasRole(string $role): bool
{
- $this->admin = $admin;
+ return $this->role !== null && $this->role !== '' && strcasecmp($this->role, $role) === 0;
}
public function set(string $name, string|null $value): void
@@ -222,6 +237,7 @@ public function addProperty(UserPropertiesModel $property): void
public function withPasswordDefinition(PasswordDefinition $passwordDefinition): static
{
$this->passwordDefinition = $passwordDefinition;
+ $this->setPassword($this->password);
return $this;
}
}
diff --git a/src/Model/UserPropertiesModel.php b/src/Model/UserPropertiesModel.php
index 2d38c66..cf2ad4f 100644
--- a/src/Model/UserPropertiesModel.php
+++ b/src/Model/UserPropertiesModel.php
@@ -2,39 +2,49 @@
namespace ByJG\Authenticate\Model;
-use ByJG\MicroOrm\Literal\HexUuidLiteral;
+use ByJG\MicroOrm\Attributes\FieldAttribute;
+use ByJG\MicroOrm\Attributes\TableAttribute;
+use ByJG\MicroOrm\Literal\Literal;
+#[TableAttribute(tableName: 'users_property')]
class UserPropertiesModel
{
- protected string|int|HexUuidLiteral|null $userid = null;
+ #[FieldAttribute]
+ protected string|int|Literal|null $userid = null;
+
+ #[FieldAttribute(primaryKey: true)]
protected ?string $id = null;
+
+ #[FieldAttribute]
protected ?string $name = null;
+
+ #[FieldAttribute]
protected ?string $value = null;
/**
* UserPropertiesModel constructor.
*
- * @param string $name
- * @param string $value
+ * @param string|null $name
+ * @param string|null $value
*/
- public function __construct(string $name = "", string $value = "")
+ public function __construct(?string $name = null, ?string $value = null)
{
$this->name = $name;
$this->value = $value;
}
/**
- * @return string|int|HexUuidLiteral|null
+ * @return string|int|Literal|null
*/
- public function getUserid(): string|int|HexUuidLiteral|null
+ public function getUserid(): string|int|Literal|null
{
return $this->userid;
}
/**
- * @param string|int|HexUuidLiteral|null $userid
+ * @param string|int|Literal|null $userid
*/
- public function setUserid(string|int|HexUuidLiteral|null $userid): void
+ public function setUserid(string|int|Literal|null $userid): void
{
$this->userid = $userid;
}
diff --git a/src/Model/UserToken.php b/src/Model/UserToken.php
new file mode 100644
index 0000000..06759b8
--- /dev/null
+++ b/src/Model/UserToken.php
@@ -0,0 +1,17 @@
+user = $user;
+ $this->token = $token;
+ $this->data = $data;
+ }
+}
\ No newline at end of file
diff --git a/src/Repository/UserPropertiesRepository.php b/src/Repository/UserPropertiesRepository.php
new file mode 100644
index 0000000..874bc9b
--- /dev/null
+++ b/src/Repository/UserPropertiesRepository.php
@@ -0,0 +1,225 @@
+mapper = new Mapper($propertiesClass);
+ $this->repository = new Repository($executor, $propertiesClass);
+ }
+
+ /**
+ * Save a property
+ *
+ * @param UserPropertiesModel $model
+ * @return UserPropertiesModel
+ * @throws DatabaseException
+ * @throws DbDriverNotConnected
+ * @throws FileException
+ * @throws InvalidArgumentException
+ * @throws OrmBeforeInvalidException
+ * @throws OrmInvalidFieldsException
+ * @throws RepositoryReadOnlyException
+ * @throws XmlUtilException
+ * @throws \ByJG\MicroOrm\Exception\InvalidArgumentException
+ * @throws UpdateConstraintException
+ */
+ public function save(UserPropertiesModel $model): UserPropertiesModel
+ {
+ $this->repository->save($model);
+ return $model;
+ }
+
+ /**
+ * Get properties by user ID
+ *
+ * @param string|Literal|int $userid
+ * @return UserPropertiesModel[]
+ * @throws DatabaseException
+ * @throws DbDriverNotConnected
+ * @throws FileException
+ * @throws XmlUtilException
+ * @throws InvalidArgumentException
+ */
+ public function getByUserId(string|Literal|int $userid): array
+ {
+ $userIdMapping = $this->mapper->getFieldMap(UserPropertyField::Userid->value);
+ $userIdField = $userIdMapping->getFieldName();
+ $query = Query::getInstance()
+ ->table($this->mapper->getTable())
+ ->where("$userIdField = :userid", ['userid' => $userIdMapping->getUpdateFunctionValue($userid, null)]);
+
+ return $this->repository->getByQuery($query);
+ }
+
+ /**
+ * Get specific property by user ID and name
+ *
+ * @param string|Literal|int $userid
+ * @param string $propertyName
+ * @return UserPropertiesModel[]
+ * @throws DatabaseException
+ * @throws DbDriverNotConnected
+ * @throws FileException
+ * @throws XmlUtilException
+ * @throws InvalidArgumentException
+ */
+ public function getByUserIdAndName(string|Literal|int $userid, string $propertyName): array
+ {
+ $userIdMapping = $this->mapper->getFieldMap(UserPropertyField::Userid->value);
+ $nameMapping = $this->mapper->getFieldMap(UserPropertyField::Name->value);
+
+ $userIdField = $userIdMapping->getFieldName();
+ $nameField = $nameMapping->getFieldName();
+
+ $query = Query::getInstance()
+ ->table($this->mapper->getTable())
+ ->where("$userIdField = :userid", ['userid' => $userIdMapping->getUpdateFunctionValue($userid, null)])
+ ->where("$nameField = :name", ['name' => $propertyName]);
+
+ return $this->repository->getByQuery($query);
+ }
+
+ /**
+ * Delete all properties for a user
+ *
+ * @param string|Literal|int $userid
+ * @return void
+ * @throws DatabaseException
+ * @throws DbDriverNotConnected
+ * @throws RepositoryReadOnlyException
+ * @throws \ByJG\MicroOrm\Exception\InvalidArgumentException
+ */
+ public function deleteByUserId(string|Literal|int $userid): void
+ {
+ $userIdMapping = $this->mapper->getFieldMap(UserPropertyField::Userid->value);
+ $userIdField = $userIdMapping->getFieldName();
+
+ $deleteQuery = DeleteQuery::getInstance()
+ ->table($this->mapper->getTable())
+ ->where("$userIdField = :userid", ['userid' => $userIdMapping->getUpdateFunctionValue($userid, null)]);
+
+ $this->repository->deleteByQuery($deleteQuery);
+ }
+
+ /**
+ * Delete specific property by user ID and name
+ *
+ * @param string|Literal|int $userid
+ * @param string $propertyName
+ * @param string|null $value
+ * @return void
+ * @throws DatabaseException
+ * @throws DbDriverNotConnected
+ * @throws RepositoryReadOnlyException
+ */
+ public function deleteByUserIdAndName(string|Literal|int $userid, string $propertyName, ?string $value = null): void
+ {
+ $userIdMapping = $this->mapper->getFieldMap(UserPropertyField::Userid->value);
+ $userIdField = $userIdMapping->getFieldName();
+ $nameField = $this->mapper->getFieldMap(UserPropertyField::Name->value)->getFieldName();
+ $valueField = $this->mapper->getFieldMap(UserPropertyField::Value->value)->getFieldName();
+
+ $deleteQuery = DeleteQuery::getInstance()
+ ->table($this->mapper->getTable())
+ ->where("$userIdField = :userid", ['userid' => $userIdMapping->getUpdateFunctionValue($userid, null)])
+ ->where("$nameField = :name", ['name' => $propertyName]);
+
+ if ($value !== null) {
+ $deleteQuery->where("$valueField = :value", ['value' => $value]);
+ }
+
+ $this->repository->deleteByQuery($deleteQuery);
+ }
+
+ /**
+ * Delete properties by name (all users)
+ *
+ * @param string $propertyName
+ * @param string|null $value
+ * @return void
+ * @throws DatabaseException
+ * @throws DbDriverNotConnected
+ * @throws RepositoryReadOnlyException
+ */
+ public function deleteByName(string $propertyName, ?string $value = null): void
+ {
+ $nameField = $this->mapper->getFieldMap(UserPropertyField::Name->value)->getFieldName();
+ $valueField = $this->mapper->getFieldMap(UserPropertyField::Value->value)->getFieldName();
+
+ $deleteQuery = DeleteQuery::getInstance()
+ ->table($this->mapper->getTable())
+ ->where("$nameField = :name", ['name' => $propertyName]);
+
+ if ($value !== null) {
+ $deleteQuery->where("$valueField = :value", ['value' => $value]);
+ }
+
+ $this->repository->deleteByQuery($deleteQuery);
+ }
+
+ /**
+ * Get the table name from mapper
+ *
+ * @return string
+ */
+ public function getTableName(): string
+ {
+ return $this->mapper->getTable();
+ }
+
+ /**
+ * Get the mapper
+ *
+ * @return Mapper
+ */
+ public function getMapper(): Mapper
+ {
+ return $this->mapper;
+ }
+
+ /**
+ * Get the underlying repository
+ *
+ * @return Repository
+ */
+ public function getRepository(): Repository
+ {
+ return $this->repository;
+ }
+}
diff --git a/src/Repository/UsersRepository.php b/src/Repository/UsersRepository.php
new file mode 100644
index 0000000..fe84c40
--- /dev/null
+++ b/src/Repository/UsersRepository.php
@@ -0,0 +1,177 @@
+mapper = new Mapper($usersClass);
+ $this->repository = new Repository($executor, $usersClass);
+ }
+
+ /**
+ * Save a user
+ *
+ * @param UserModel $model
+ * @return UserModel
+ * @throws DatabaseException
+ * @throws DbDriverNotConnected
+ * @throws FileException
+ * @throws InvalidArgumentException
+ * @throws OrmBeforeInvalidException
+ * @throws OrmInvalidFieldsException
+ * @throws XmlUtilException
+ * @throws RepositoryReadOnlyException
+ * @throws UpdateConstraintException
+ * @throws \Psr\SimpleCache\InvalidArgumentException
+ */
+ public function save(UserModel $model): UserModel
+ {
+ $this->repository->save($model);
+ return $model;
+ }
+
+ /**
+ * Get user by ID
+ *
+ * @param string|Literal|int $userid
+ * @return UserModel|null
+ * @throws DatabaseException
+ * @throws DbDriverNotConnected
+ * @throws FileException
+ * @throws InvalidArgumentException
+ * @throws OrmInvalidFieldsException
+ * @throws XmlUtilException
+ * @throws \Psr\SimpleCache\InvalidArgumentException
+ */
+ public function getById(string|Literal|int $userid): ?UserModel
+ {
+ return $this->repository->get($userid);
+ }
+
+ /**
+ * Get user by field value
+ *
+ * @param string $field Property name (e.g., 'username', 'email')
+ * @param string|Literal|int $value
+ * @return UserModel|null
+ * @throws DatabaseException
+ * @throws DbDriverNotConnected
+ * @throws FileException
+ * @throws XmlUtilException
+ * @throws \Psr\SimpleCache\InvalidArgumentException
+ */
+ public function getByField(string $field, string|Literal|int $value): ?UserModel
+ {
+ // Map the property name to the actual database column name
+ $fieldMapping = $this->mapper->getFieldMap($field);
+ $dbColumnName = $fieldMapping ? $fieldMapping->getFieldName() : $field;
+
+ $query = Query::getInstance()
+ ->table($this->mapper->getTable())
+ ->where("$dbColumnName = :value", ['value' => $value]);
+
+ $result = $this->repository->getByQuery($query);
+ return count($result) > 0 ? $result[0] : null;
+ }
+
+ /**
+ * Get all users
+ *
+ * @return UserModel[]
+ * @throws DatabaseException
+ * @throws DbDriverNotConnected
+ * @throws FileException
+ * @throws XmlUtilException
+ * @throws \Psr\SimpleCache\InvalidArgumentException
+ */
+ public function getAll(): array
+ {
+ $query = Query::getInstance()->table($this->mapper->getTable());
+ return $this->repository->getByQuery($query);
+ }
+
+ /**
+ * Delete user by ID
+ *
+ * @param string|Literal|int $userid
+ * @return void
+ * @throws \Exception
+ */
+ public function deleteById(string|Literal|int $userid): void
+ {
+ $this->repository->delete($userid);
+ }
+
+ /**
+ * Get the table name from mapper
+ *
+ * @return string
+ */
+ public function getTableName(): string
+ {
+ return $this->mapper->getTable();
+ }
+
+ /**
+ * Get the primary key field name from mapper
+ *
+ * @return string|array
+ */
+ public function getPrimaryKeyName(): string|array
+ {
+ return $this->mapper->getPrimaryKey();
+ }
+
+ /**
+ * Get the mapper
+ *
+ * @return Mapper
+ */
+ public function getMapper(): Mapper
+ {
+ return $this->mapper;
+ }
+
+ /**
+ * Get the underlying repository
+ *
+ * @return Repository
+ */
+ public function getRepository(): Repository
+ {
+ return $this->repository;
+ }
+}
diff --git a/src/Service/UsersService.php b/src/Service/UsersService.php
new file mode 100644
index 0000000..53cc21f
--- /dev/null
+++ b/src/Service/UsersService.php
@@ -0,0 +1,600 @@
+usersRepository = $usersRepository;
+ $this->propertiesRepository = $propertiesRepository;
+ $this->passwordDefinition = $passwordDefinition;
+ $this->loginField = $loginField;
+
+ $userMapper = $usersRepository->getRepository()->getMapper();
+ $userCheck = ($userMapper->getFieldMap(UserField::Userid->value) !== null) &&
+ ($userMapper->getFieldMap(UserField::Name->value) !== null) &&
+ ($userMapper->getFieldMap(UserField::Email->value) !== null) &&
+ ($userMapper->getFieldMap(UserField::Username->value) !== null) &&
+ ($userMapper->getFieldMap(UserField::Password->value) !== null) &&
+ ($userMapper->getFieldMap(UserField::Role->value) !== null);
+
+ if (!$userCheck) {
+ throw new InvalidArgumentException('Invalid user repository field mappings');
+ }
+
+ // Validate password mapper implements PasswordMapperInterface (handles both string class name and instance)
+ $passwordUpdateFunction = $userMapper->getFieldMap(UserField::Password->value)->getUpdateFunction();
+ if (!is_subclass_of($passwordUpdateFunction, PasswordMapperInterface::class, true)) {
+ throw new InvalidArgumentException('Password update function must implement PasswordMapperInterface');
+ }
+
+ $this->passwordDefinition?->setPasswordMapper($passwordUpdateFunction);
+
+ $propertyMapper = $propertiesRepository->getRepository()->getMapper();
+ $propertyCheck = ($propertyMapper->getFieldMap(UserPropertyField::Userid->value) !== null) &&
+ ($propertyMapper->getFieldMap(UserPropertyField::Name->value) !== null) &&
+ ($propertyMapper->getFieldMap(UserPropertyField::Value->value) !== null);
+
+ if (!$propertyCheck) {
+ throw new InvalidArgumentException('Invalid property repository field mappings');
+ }
+ }
+
+ /**
+ * Get the user's repository
+ *
+ * @return UsersRepository
+ */
+ public function getUsersRepository(): UsersRepository
+ {
+ return $this->usersRepository;
+ }
+
+ /**
+ * Get the property repository
+ *
+ * @return UserPropertiesRepository
+ */
+ public function getPropertiesRepository(): UserPropertiesRepository
+ {
+ return $this->propertiesRepository;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ #[\Override]
+ public function save(UserModel $model): UserModel
+ {
+ $newUser = false;
+ if (empty($model->getUserid())) {
+ $this->canAddUser($model);
+ $newUser = true;
+ }
+
+ $this->usersRepository->save($model);
+
+ // Save properties
+ foreach ($model->getProperties() as $property) {
+ $property->setUserid($model->getUserid());
+ $this->propertiesRepository->save($property);
+ }
+
+ if ($newUser) {
+ $model = $this->getById($model->getUserid());
+ }
+
+ if ($model === null) {
+ throw new UserNotFoundException("User not found");
+ }
+
+ return $model;
+ }
+
+ /**
+ * @param string|null $role
+ * @inheritDoc
+ */
+ #[\Override]
+ public function addUser(string $name, string $userName, string $email, string $password, ?string $role = null): UserModel
+ {
+
+ $mapper = $this->usersRepository->getMapper();
+ $model = $mapper->getEntity([
+ $mapper->getFieldMap(UserField::Name->value)->getFieldName() => $name,
+ $mapper->getFieldMap(UserField::Email->value)->getFieldName() => $email,
+ $mapper->getFieldMap(UserField::Username->value)->getFieldName() => $userName,
+ $mapper->getFieldMap(UserField::Role->value)->getFieldName() => $role
+ ]);
+ $this->applyPasswordDefinition($model);
+ $model->setPassword($password);
+
+ return $this->save($model);
+ }
+
+ /**
+ * Check if user can be added (doesn't exist already)
+ *
+ * @param UserModel $model
+ * @return bool
+ * @throws UserExistsException
+ */
+ protected function canAddUser(UserModel $model): bool
+ {
+ if (!empty($model->getUserid()) && $this->getById($model->getUserid()) !== null) {
+ throw new UserExistsException('User ID already exists');
+ }
+
+ if ($this->getByUsername($model->getUsername()) !== null) {
+ throw new UserExistsException('Username already exists');
+ }
+
+ return true;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ #[\Override]
+ public function getById(string|Literal|int $userid): ?UserModel
+ {
+ $user = $this->usersRepository->getById($userid);
+ if ($user !== null) {
+ $this->loadUserProperties($user);
+ $this->applyPasswordDefinition($user);
+ }
+ return $user;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ #[\Override]
+ public function getByEmail(string $email): ?UserModel
+ {
+ $fieldMap = $this->usersRepository->getMapper()->getFieldMap(UserField::Email->value);
+ $user = $this->usersRepository->getByField($fieldMap->getFieldName(), $email);
+ if ($user !== null) {
+ $this->loadUserProperties($user);
+ $this->applyPasswordDefinition($user);
+ }
+ return $user;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ #[\Override]
+ public function getByUsername(string $username): ?UserModel
+ {
+ $fieldMap = $this->usersRepository->getMapper()->getFieldMap(UserField::Username->value);
+ $user = $this->usersRepository->getByField($fieldMap->getFieldName(), $username);
+ if ($user !== null) {
+ $this->loadUserProperties($user);
+ $this->applyPasswordDefinition($user);
+ }
+ return $user;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ #[\Override]
+ public function getByLogin(string $login): ?UserModel
+ {
+ return $this->loginField === LoginField::Email
+ ? $this->getByEmail($login)
+ : $this->getByUsername($login);
+ }
+
+ /**
+ * Load user properties into user model
+ *
+ * @param UserModel $user
+ * @return void
+ */
+ protected function loadUserProperties(UserModel $user): void
+ {
+ $properties = $this->propertiesRepository->getByUserId($user->getUserid());
+ $user->setProperties($properties);
+ }
+
+ protected function applyPasswordDefinition(UserModel $user): void
+ {
+ if ($this->passwordDefinition !== null) {
+ $user->withPasswordDefinition($this->passwordDefinition);
+ }
+ }
+
+ protected function resolveUserFieldValue(UserModel $user, string $fieldName, ?string $default = null): mixed
+ {
+ $camel = str_replace(' ', '', ucwords(str_replace(['_', '-'], ' ' , $fieldName)));
+ $method = 'get' . $camel;
+ if (method_exists($user, $method)) {
+ $value = $user->$method();
+ } else {
+ $value = $user->get($fieldName);
+ }
+
+ return empty($value) ? $default : $value;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ #[\Override]
+ public function removeByLogin(string $login): bool
+ {
+ $user = $this->getByLogin($login);
+ if ($user !== null) {
+ return $this->removeById($user->getUserid());
+ }
+ return false;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ #[\Override]
+ public function removeById(string|Literal|int $userid): bool
+ {
+ try {
+ // Delete properties first
+ $this->propertiesRepository->deleteByUserId($userid);
+ // Delete user
+ $this->usersRepository->deleteById($userid);
+ return true;
+ } catch (Exception $e) {
+ return false;
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ #[\Override]
+ public function isValidUser(string $login, string $password): ?UserModel
+ {
+ $user = $this->getByLogin($login);
+
+ if ($user === null) {
+ return null;
+ }
+
+ // Hash the password for comparison using the model's configured password mapper
+ $passwordFieldMapping = $this->usersRepository->getMapper()->getFieldMap(UserField::Password->value);
+ $hashedPassword = $passwordFieldMapping->getUpdateFunctionValue($password, null);
+
+ if ($user->getPassword() === $hashedPassword) {
+ return $user;
+ }
+
+ return null;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ #[\Override]
+ public function hasProperty(string|int|Literal $userId, string $propertyName, ?string $value = null): bool
+ {
+ $user = $this->getById($userId);
+
+ if (empty($user)) {
+ return false;
+ }
+
+ $values = $user->get($propertyName);
+
+ if ($values === null) {
+ return false;
+ }
+
+ if ($value === null) {
+ return true;
+ }
+
+ return in_array($value, (array)$values);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ #[\Override]
+ public function getProperty(string|Literal|int $userId, string $propertyName): array|string|UserPropertiesModel|null
+ {
+ $properties = $this->propertiesRepository->getByUserIdAndName($userId, $propertyName);
+
+ if (count($properties) === 0) {
+ return null;
+ }
+
+ $result = [];
+ foreach ($properties as $property) {
+ $result[] = $property->getValue();
+ }
+
+ if (count($result) === 1) {
+ return $result[0];
+ }
+
+ return $result;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ #[\Override]
+ public function addProperty(string|Literal|int $userId, string $propertyName, ?string $value): bool
+ {
+ $user = $this->getById($userId);
+ if (empty($user)) {
+ return false;
+ }
+
+ if (!$this->hasProperty($userId, $propertyName, $value)) {
+ $propMapper = $this->propertiesRepository->getMapper();
+ $property = $propMapper->getEntity([
+ $propMapper->getFieldMap(UserPropertyField::Userid->value)->getFieldName() => $userId,
+ $propMapper->getFieldMap(UserPropertyField::Name->value)->getFieldName() => $propertyName,
+ $propMapper->getFieldMap(UserPropertyField::Value->value)->getFieldName() => $value
+ ]);
+ $this->propertiesRepository->save($property);
+ }
+
+ return true;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ #[\Override]
+ public function setProperty(string|Literal|int $userId, string $propertyName, ?string $value): bool
+ {
+ $properties = $this->propertiesRepository->getByUserIdAndName($userId, $propertyName);
+
+ if (empty($properties)) {
+ $propMapper = $this->propertiesRepository->getMapper();
+ $property = $propMapper->getEntity([
+ $propMapper->getFieldMap(UserPropertyField::Userid->value)->getFieldName() => $userId,
+ $propMapper->getFieldMap(UserPropertyField::Name->value)->getFieldName() => $propertyName,
+ $propMapper->getFieldMap(UserPropertyField::Value->value)->getFieldName() => $value
+ ]);
+ } else {
+ $property = $properties[0];
+ $property->setValue($value);
+ }
+
+ $this->propertiesRepository->save($property);
+ return true;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ #[\Override]
+ public function removeProperty(string|Literal|int $userId, string $propertyName, ?string $value = null): bool
+ {
+ $user = $this->getById($userId);
+ if ($user !== null) {
+ $this->propertiesRepository->deleteByUserIdAndName($userId, $propertyName, $value);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ #[\Override]
+ public function removeAllProperties(string $propertyName, ?string $value = null): void
+ {
+ $this->propertiesRepository->deleteByName($propertyName, $value);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ #[\Override]
+ public function getUsersByProperty(string $propertyName, string $value): array
+ {
+ return $this->getUsersByPropertySet([$propertyName => $value]);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ #[\Override]
+ public function getUsersByPropertySet(array $propertiesArray): array
+ {
+ $userTable = $this->usersRepository->getTableName();
+ $propTable = $this->propertiesRepository->getTableName();
+ $userPk = $this->usersRepository->getPrimaryKeyName();
+
+ // Handle composite keys - use first key if array
+ if (is_array($userPk)) {
+ $userPk = $userPk[0];
+ }
+
+ $propUserIdField = $this->propertiesRepository->getMapper()->getFieldMap(UserPropertyField::Userid->value)->getFieldName();
+ $propNameField = $this->propertiesRepository->getMapper()->getFieldMap(UserPropertyField::Name->value)->getFieldName();
+ $propValueField = $this->propertiesRepository->getMapper()->getFieldMap(UserPropertyField::Value->value)->getFieldName();
+
+ $query = Query::getInstance()
+ ->field("u.*")
+ ->table($userTable, "u");
+
+ $count = 0;
+ foreach ($propertiesArray as $propertyName => $value) {
+ $count++;
+ $query->join($propTable, "p$count.$propUserIdField = u.$userPk", "p$count")
+ ->where("p$count.$propNameField = :name$count", ["name$count" => $propertyName])
+ ->where("p$count.$propValueField = :value$count", ["value$count" => $value]);
+ }
+
+ $users = $this->usersRepository->getRepository()->getByQuery($query);
+ if ($this->passwordDefinition !== null) {
+ foreach ($users as $user) {
+ $this->applyPasswordDefinition($user);
+ }
+ }
+ return $users;
+ }
+
+ #[\Override]
+ public function createInsecureAuthToken(
+ string|UserModel $login,
+ JwtWrapper $jwtWrapper,
+ int $expires = 1200,
+ array $updateUserInfo = [],
+ array $updateTokenInfo = [],
+ array $tokenUserFields = [UserField::Userid, UserField::Name, UserField::Role]
+ ): ?UserToken
+ {
+ if (is_string($login)) {
+ $user = $this->getByLogin($login);
+ } else {
+ $user = $login;
+ }
+
+ if (is_null($user)) {
+ throw new UserNotFoundException('User not found');
+ }
+
+ foreach ($updateUserInfo as $key => $value) {
+ $user->set($key, $value);
+ }
+
+ foreach ($tokenUserFields as $key => $field) {
+ $default = null;
+ if (!is_numeric($key)) {
+ $default = $field;
+ $field = $key;
+ }
+ $fieldName = $field instanceof UserField ? $field->value : $field;
+ if (!is_string($fieldName) || $fieldName === '') {
+ continue;
+ }
+
+ $value = $this->resolveUserFieldValue($user, $fieldName, $default);
+ if ($value !== null) {
+ $updateTokenInfo[$fieldName] = $value;
+ }
+ }
+
+ $jwtData = $jwtWrapper->createJwtData($updateTokenInfo, $expires);
+
+ $token = $jwtWrapper->generateToken($jwtData);
+
+ $user->set('TOKEN_HASH', sha1($token));
+ $this->save($user);
+
+ return new UserToken(
+ user: $user,
+ token: $token,
+ data: $updateTokenInfo
+ );
+ }
+
+ /**
+ * @inheritDoc
+ */
+ #[\Override]
+ public function createAuthToken(
+ string $login,
+ string $password,
+ JwtWrapper $jwtWrapper,
+ int $expires = 1200,
+ array $updateUserInfo = [],
+ array $updateTokenInfo = [],
+ array $tokenUserFields = [UserField::Userid, UserField::Name, UserField::Role]
+ ): ?UserToken
+ {
+ $user = $this->isValidUser($login, $password);
+ if (is_null($user)) {
+ throw new UserNotFoundException('User not found');
+ }
+
+ return $this->createInsecureAuthToken(
+ $user,
+ $jwtWrapper,
+ $expires,
+ $updateUserInfo,
+ $updateTokenInfo,
+ $tokenUserFields
+ );
+ }
+
+ /**
+ * @inheritDoc
+ * @throws JwtWrapperException
+ * @throws NotAuthenticatedException
+ * @throws UserNotFoundException
+ */
+ #[\Override]
+ public function isValidToken(string $login, JwtWrapper $jwtWrapper, string $token): UserToken
+ {
+ $user = $this->getByLogin($login);
+
+ if (is_null($user)) {
+ throw new UserNotFoundException('User not found!');
+ }
+
+ if ($user->get('TOKEN_HASH') !== sha1($token)) {
+ throw new NotAuthenticatedException('Token does not match');
+ }
+
+ $data = $jwtWrapper->extractData($token);
+
+ $this->save($user);
+
+ return new UserToken(
+ user: $user,
+ token: $token,
+ data: (array)$data->data
+ );
+ }
+
+ #[\Override]
+ public function getUsersEntity(array $fields): UserModel
+ {
+ $entity = $this->getUsersRepository()->getMapper()->getEntity($fields);
+ $this->applyPasswordDefinition($entity);
+ return $entity;
+ }
+}
diff --git a/src/SessionContext.php b/src/SessionContext.php
index fe7a115..214d134 100644
--- a/src/SessionContext.php
+++ b/src/SessionContext.php
@@ -5,6 +5,7 @@
use ByJG\Authenticate\Exception\NotAuthenticatedException;
use ByJG\Authenticate\Interfaces\UserContextInterface;
use ByJG\Cache\Psr6\CachePool;
+use Override;
use Psr\SimpleCache\InvalidArgumentException;
class SessionContext implements UserContextInterface
@@ -39,6 +40,7 @@ public function __construct(CachePool $cachePool, string $key = 'default')
* @return bool Return true if authenticated; false otherwise.
* @throws InvalidArgumentException
*/
+ #[Override]
public function isAuthenticated(): bool
{
$item = $this->session->getItem("user.$this->key");
@@ -52,6 +54,7 @@ public function isAuthenticated(): bool
* @return string|int The authenticated username if exists.
* @throws InvalidArgumentException
*/
+ #[Override]
public function userInfo(): string|int
{
$item = $this->session->getItem("user.$this->key");
@@ -63,6 +66,7 @@ public function userInfo(): string|int
* @param array $data
* @throws InvalidArgumentException
*/
+ #[Override]
public function registerLogin(string|int $userId, array $data = []): void
{
$item = $this->session->getItem("user.$this->key");
@@ -84,6 +88,7 @@ public function registerLogin(string|int $userId, array $data = []): void
* @throws InvalidArgumentException
* @throws InvalidArgumentException
*/
+ #[Override]
public function setSessionData(string $name, mixed $value): void
{
if (!$this->isAuthenticated()) {
@@ -110,6 +115,7 @@ public function setSessionData(string $name, mixed $value): void
* @throws InvalidArgumentException
* @throws InvalidArgumentException
*/
+ #[Override]
public function getSessionData(string $name): mixed
{
if (!$this->isAuthenticated()) {
@@ -137,6 +143,7 @@ public function getSessionData(string $name): mixed
* @throws InvalidArgumentException
* @throws InvalidArgumentException
*/
+ #[Override]
public function registerLogout(): void
{
$this->session->deleteItem("user.$this->key");
diff --git a/src/UsersAnyDataset.php b/src/UsersAnyDataset.php
deleted file mode 100644
index 64a253b..0000000
--- a/src/UsersAnyDataset.php
+++ /dev/null
@@ -1,317 +0,0 @@
-anyDataSet = $anyDataset;
- $this->anyDataSet->save();
- $this->userTable = $userTable;
- if (!$userTable->existsClosure('update', UserDefinition::FIELD_USERID)) {
- $userTable->defineClosureForUpdate(UserDefinition::FIELD_USERID, function ($value, $instance) {
- if (empty($value)) {
- return preg_replace('/(?:([\w])|([\W]))/', '\1', strtolower($instance->getUsername()));
- }
- return $value;
- });
- }
- $this->propertiesTable = $propertiesTable;
- }
-
- /**
- * Save the current UsersAnyDataset
- *
- * @param UserModel $model
- * @return UserModel
- * @throws DatabaseException
- * @throws InvalidArgumentException
- * @throws UserExistsException
- * @throws FileException
- */
- public function save(UserModel $model): UserModel
- {
- $new = true;
- if (!empty($model->getUserid())) {
- $new = !$this->removeUserById($model->getUserid());
- }
-
- $new && $this->canAddUser($model);
-
- $this->anyDataSet->appendRow();
-
- $propertyDefinition = $this->getUserDefinition()->toArray();
- foreach ($propertyDefinition as $property => $map) {
- $closure = $this->getUserDefinition()->getClosureForUpdate($property);
- $value = $closure($model->{"get$property"}(), $model);
- if ($value !== false) {
- $this->anyDataSet->addField($map, $value);
- }
- }
-
- $properties = $model->getProperties();
- foreach ($properties as $value) {
- $this->anyDataSet->addField($value->getName(), $value->getValue());
- }
-
- $this->anyDataSet->save();
-
- return $model;
- }
-
- /**
- * Get the user based on a filter.
- * Return Row if user was found; null, otherwise
- *
- * @param IteratorFilter $filter Filter to find user
- * @return UserModel|null
- * @throws InvalidArgumentException
- */
- public function getUser(IteratorFilter $filter): UserModel|null
- {
- $iterator = $this->anyDataSet->getIterator($filter);
- if (!$iterator->hasNext()) {
- return null;
- }
-
- return $this->createUserModel($iterator->moveNext());
- }
-
- /**
- * Get the user based on his login.
- * Return Row if user was found; null, otherwise
- *
- * @param string $login
- * @return boolean
- * @throws DatabaseException
- * @throws FileException
- * @throws InvalidArgumentException
- */
- public function removeByLoginField(string $login): bool
- {
- //anydataset.Row
- $iteratorFilter = new IteratorFilter();
- $iteratorFilter->and($this->getUserDefinition()->loginField(), Relation::EQUAL, $login);
- $iterator = $this->anyDataSet->getIterator($iteratorFilter);
-
- if ($iterator->hasNext()) {
- $oldRow = $iterator->moveNext();
- $this->anyDataSet->removeRow($oldRow);
- $this->anyDataSet->save();
- return true;
- }
-
- return false;
- }
-
- /**
- * Get an Iterator based on a filter
- *
- * @param IteratorFilter|null $filter
- * @return IteratorInterface
- */
- public function getIterator(IteratorFilter $filter = null): IteratorInterface
- {
- return $this->anyDataSet->getIterator($filter);
- }
-
- /**
- * @throws InvalidArgumentException
- */
- public function getUsersByProperty(string $propertyName, string $value): array
- {
- return $this->getUsersByPropertySet([$propertyName => $value]);
- }
-
- /**
- * @throws InvalidArgumentException
- */
- public function getUsersByPropertySet(array $propertiesArray): array
- {
- $filter = new IteratorFilter();
- foreach ($propertiesArray as $propertyName => $value) {
- $filter->and($propertyName, Relation::EQUAL, $value);
- }
- $result = [];
- foreach ($this->getIterator($filter) as $model) {
- $result[] = $this->createUserModel($model);
- }
- return $result;
- }
-
- /**
- * @param string|int|HexUuidLiteral $userId
- * @param string $propertyName
- * @param string|null $value
- * @return boolean
- * @throws DatabaseException
- * @throws FileException
- * @throws InvalidArgumentException
- * @throws UserExistsException
- * @throws UserNotFoundException
- */
- public function addProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value): bool
- {
- //anydataset.Row
- $user = $this->getById($userId);
- if ($user !== null) {
- if (!$this->hasProperty($user->getUserid(), $propertyName, $value)) {
- $user->addProperty(new UserPropertiesModel($propertyName, $value));
- $this->save($user);
- }
- return true;
- }
-
- return false;
- }
-
- /**
- * @throws InvalidArgumentException
- * @throws DatabaseException
- * @throws UserExistsException
- * @throws FileException
- */
- public function setProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value): bool
- {
- $user = $this->getById($userId);
- if ($user !== null) {
- $user->set($propertyName, $value);
- $this->save($user);
- return true;
- }
- return false;
- }
-
- /**
- * @param string|int|HexUuidLiteral $userId
- * @param string $propertyName
- * @param string|null $value
- * @return boolean
- * @throws DatabaseException
- * @throws FileException
- * @throws InvalidArgumentException
- * @throws UserExistsException
- */
- public function removeProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value = null): bool
- {
- $user = $this->getById($userId);
- if (!empty($user)) {
- $properties = $user->getProperties();
- foreach ($properties as $key => $property) {
- if ($property->getName() == $propertyName && (empty($value) || $property->getValue() == $value)) {
- unset($properties[$key]);
- }
- }
- $user->setProperties($properties);
- $this->save($user);
- return true;
- }
-
- return false;
- }
-
- /**
- * Remove a specific site from all users
- * Return True or false
- *
- * @param string $propertyName Property name
- * @param string|null $value Property value with a site
- * @return void
- * @throws DatabaseException
- * @throws FileException
- * @throws InvalidArgumentException
- * @throws UserExistsException
- */
- public function removeAllProperties(string $propertyName, string|null $value = null): void
- {
- $iterator = $this->getIterator(null);
- while ($iterator->hasNext()) {
- //anydataset.Row
- $user = $iterator->moveNext();
- $this->removeProperty($user->get($this->getUserDefinition()->getUserid()), $propertyName, $value);
- }
- }
-
- /**
- * @param Row $row
- * @return UserModel
- * @throws InvalidArgumentException
- */
- private function createUserModel(Row $row): UserModel
- {
- $allProp = $row->toArray();
- $userModel = new UserModel();
-
- $userTableProp = $this->getUserDefinition()->toArray();
- foreach ($userTableProp as $prop => $mapped) {
- if (isset($allProp[$mapped])) {
- $userModel->{"set" . ucfirst($prop)}($allProp[$mapped]);
- unset($allProp[$mapped]);
- }
- }
-
- foreach (array_keys($allProp) as $property) {
- foreach ($row->getAsArray($property) as $eachValue) {
- $userModel->addProperty(new UserPropertiesModel($property, $eachValue));
- }
- }
-
- return $userModel;
- }
-
- /**
- * @param string|HexUuidLiteral|int $userid
- * @return bool
- * @throws InvalidArgumentException
- */
- public function removeUserById(string|HexUuidLiteral|int $userid): bool
- {
- $iteratorFilter = new IteratorFilter();
- $iteratorFilter->and($this->getUserDefinition()->getUserid(), Relation::EQUAL, $userid);
- $iterator = $this->anyDataSet->getIterator($iteratorFilter);
-
- if ($iterator->hasNext()) {
- $oldRow = $iterator->moveNext();
- $this->anyDataSet->removeRow($oldRow);
- return true;
- }
-
- return false;
- }
-}
diff --git a/src/UsersBase.php b/src/UsersBase.php
deleted file mode 100644
index e8ed42d..0000000
--- a/src/UsersBase.php
+++ /dev/null
@@ -1,402 +0,0 @@
-userTable === null) {
- $this->userTable = new UserDefinition();
- }
- return $this->userTable;
- }
-
- /**
- * @return UserPropertiesDefinition
- */
- public function getUserPropertiesDefinition(): UserPropertiesDefinition
- {
- if ($this->propertiesTable === null) {
- $this->propertiesTable = new UserPropertiesDefinition();
- }
- return $this->propertiesTable;
- }
-
- /**
- * Save the current UsersAnyDataset
- *
- * @param UserModel $model
- */
- abstract public function save(UserModel $model): UserModel;
-
- /**
- * Add new user in database
- *
- * @param string $name
- * @param string $userName
- * @param string $email
- * @param string $password
- * @return UserModel
- */
- public function addUser(string $name, string $userName, string $email, string $password): UserModel
- {
- $model = $this->getUserDefinition()->modelInstance();
- $model->setName($name);
- $model->setEmail($email);
- $model->setUsername($userName);
- $model->setPassword($password);
-
- return $this->save($model);
- }
-
- /**
- * @param UserModel $model
- * @return bool
- * @throws UserExistsException
- * @throws InvalidArgumentException
- */
- public function canAddUser(UserModel $model): bool
- {
- if ($this->getByEmail($model->getEmail()) !== null) {
- throw new UserExistsException('Email already exists');
- }
- $filter = new IteratorFilter();
- $filter->and($this->getUserDefinition()->getUsername(), Relation::EQUAL, $model->getUsername());
- if ($this->getUser($filter) !== null) {
- throw new UserExistsException('Username already exists');
- }
-
- return false;
- }
-
- /**
- * Get the user based on a filter.
- * Return Row if user was found; null, otherwise
- *
- * @param IteratorFilter $filter Filter to find user
- * @return UserModel|null
- * */
- abstract public function getUser(IteratorFilter $filter): UserModel|null;
-
- /**
- * Get the user based on his email.
- * Return Row if user was found; null, otherwise
- *
- * @param string $email
- * @return UserModel|null
- * @throws InvalidArgumentException
- */
- public function getByEmail(string $email): UserModel|null
- {
- $filter = new IteratorFilter();
- $filter->and($this->getUserDefinition()->getEmail(), Relation::EQUAL, strtolower($email));
- return $this->getUser($filter);
- }
-
- /**
- * Get the user based on his username.
- * Return Row if user was found; null, otherwise
- *
- * @param string $username
- * @return UserModel|null
- * @throws InvalidArgumentException
- */
- public function getByUsername(string $username): UserModel|null
- {
- $filter = new IteratorFilter();
- $filter->and($this->getUserDefinition()->getUsername(), Relation::EQUAL, $username);
- return $this->getUser($filter);
- }
-
- /**
- * Get the user based on his login.
- * Return Row if user was found; null, otherwise
- *
- * @param string $login
- * @return UserModel|null
- */
- public function getByLoginField(string $login): UserModel|null
- {
- $filter = new IteratorFilter();
- $filter->and($this->getUserDefinition()->loginField(), Relation::EQUAL, strtolower($login));
-
- return $this->getUser($filter);
- }
-
- /**
- * Get the user based on his id.
- * Return Row if user was found; null, otherwise
- *
- * @param string|HexUuidLiteral|int $userid
- * @return UserModel|null
- * @throws InvalidArgumentException
- */
- public function getById(string|HexUuidLiteral|int $userid): UserModel|null
- {
- $filter = new IteratorFilter();
- $filter->and($this->getUserDefinition()->getUserid(), Relation::EQUAL, $userid);
- return $this->getUser($filter);
- }
-
- /**
- * Remove the user based on his login.
- *
- * @param string $login
- * @return bool
- * */
- abstract public function removeByLoginField(string $login): bool;
-
- /**
- * Validate if the user and password exists in the file
- * Return Row if user exists; null, otherwise
- *
- * @param string $userName User login
- * @param string $password Plain text password
- * @return UserModel|null
- * @throws InvalidArgumentException
- */
- public function isValidUser(string $userName, string $password): UserModel|null
- {
- $filter = new IteratorFilter();
- $passwordGenerator = $this->getUserDefinition()->getClosureForUpdate(UserDefinition::FIELD_PASSWORD);
- $filter->and($this->getUserDefinition()->loginField(), Relation::EQUAL, strtolower($userName));
- $filter->and(
- $this->getUserDefinition()->getPassword(),
- Relation::EQUAL,
- $passwordGenerator($password, null)
- );
- return $this->getUser($filter);
- }
-
- /**
- * Check if the user have a property and it has a specific value.
- * Return True if you have rights; false, otherwise
- *
- * @param string|int|HexUuidLiteral|null $userId User identification
- * @param string $propertyName
- * @param string|null $value Property value
- * @return bool
- * @throws UserNotFoundException
- * @throws InvalidArgumentException
- */
- public function hasProperty(string|int|HexUuidLiteral|null $userId, string $propertyName, string $value = null): bool
- {
- //anydataset.Row
- $user = $this->getById($userId);
-
- if (empty($user)) {
- return false;
- }
-
- if ($this->isAdmin($userId)) {
- return true;
- }
-
- $values = $user->get($propertyName);
-
- if ($values === null) {
- return false;
- }
-
- if ($value === null) {
- return true;
- }
-
- return in_array($value, (array)$values);
- }
-
- /**
- * Return all sites from a specific user
- * Return String vector with all sites
- *
- * @param string|int|HexUuidLiteral $userId User ID
- * @param string $propertyName Property name
- * @return array|string|UserPropertiesModel|null
- * @throws UserNotFoundException
- * @throws InvalidArgumentException
- */
- public function getProperty(string|HexUuidLiteral|int $userId, string $propertyName): array|string|UserPropertiesModel|null
- {
- $user = $this->getById($userId);
- if ($user !== null) {
- $values = $user->get($propertyName);
-
- if ($this->isAdmin($userId)) {
- return array(UserDefinition::FIELD_ADMIN => "admin");
- }
-
- return $values;
- }
-
- return null;
- }
-
- abstract public function getUsersByProperty(string $propertyName, string $value): array;
-
- abstract public function getUsersByPropertySet(array $propertiesArray): array;
-
- /**
- *
- * @param string|int|HexUuidLiteral $userId
- * @param string $propertyName
- * @param string|null $value
- */
- abstract public function addProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value): bool;
-
- /**
- * Remove a specific site from user
- * Return True or false
- *
- * @param string|int|HexUuidLiteral $userId User login
- * @param string $propertyName Property name
- * @param string|null $value Property value with a site
- * @return bool
- * */
- abstract public function removeProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value = null): bool;
-
- /**
- * Remove a specific site from all users
- * Return True or false
- *
- * @param string $propertyName Property name
- * @param string|null $value Property value with a site
- * @return void
- * */
- abstract public function removeAllProperties(string $propertyName, string|null $value = null): void;
-
- /**
- * @param string|int|HexUuidLiteral $userId
- * @return bool
- * @throws UserNotFoundException
- * @throws InvalidArgumentException
- */
- public function isAdmin(string|HexUuidLiteral|int $userId): bool
- {
- $user = $this->getById($userId);
-
- if (is_null($user)) {
- throw new UserNotFoundException("Cannot find the user");
- }
-
- return
- preg_match('/^(yes|YES|[yY]|true|TRUE|[tT]|1|[sS])$/', $user->getAdmin()) === 1
- ;
- }
-
- /**
- * Authenticate a user and create a token if it is valid
- *
- * @param string $login
- * @param string $password
- * @param JwtWrapper $jwtWrapper
- * @param int $expires
- * @param array $updateUserInfo
- * @param array $updateTokenInfo
- * @return string|null the TOKEN or false if you don't.
- * @throws UserNotFoundException
- * @throws InvalidArgumentException
- */
- public function createAuthToken(
- string $login,
- string $password,
- JwtWrapper $jwtWrapper,
- int $expires = 1200,
- array $updateUserInfo = [],
- array $updateTokenInfo = []
- ): string|null
- {
- $user = $this->isValidUser($login, $password);
- if (is_null($user)) {
- throw new UserNotFoundException('User not found');
- }
-
- foreach ($updateUserInfo as $key => $value) {
- $user->set($key, $value);
- }
-
- $updateTokenInfo['login'] = $login;
- $updateTokenInfo[UserDefinition::FIELD_USERID] = $user->getUserid();
- $jwtData = $jwtWrapper->createJwtData(
- $updateTokenInfo,
- $expires
- );
-
- $token = $jwtWrapper->generateToken($jwtData);
-
- $user->set('TOKEN_HASH', sha1($token));
- $this->save($user);
-
- return $token;
- }
-
- /**
- * Check if the Auth Token is valid
- *
- * @param string $login
- * @param JwtWrapper $jwtWrapper
- * @param string $token
- * @return array|null
- * @throws JwtWrapperException
- * @throws NotAuthenticatedException
- * @throws UserNotFoundException
- */
- public function isValidToken(string $login, JwtWrapper $jwtWrapper, string $token): array|null
- {
- $user = $this->getByLoginField($login);
-
- if (is_null($user)) {
- throw new UserNotFoundException('User not found!');
- }
-
- if ($user->get('TOKEN_HASH') !== sha1($token)) {
- throw new NotAuthenticatedException('Token does not match');
- }
-
- $data = $jwtWrapper->extractData($token);
-
- $this->save($user);
-
- return [
- 'user' => $user,
- 'data' => $data->data
- ];
- }
-
- /**
- * @param string|int|HexUuidLiteral $userid
- */
- abstract public function removeUserById(string|HexUuidLiteral|int $userid): bool;
-}
diff --git a/src/UsersDBDataset.php b/src/UsersDBDataset.php
deleted file mode 100644
index 8c29126..0000000
--- a/src/UsersDBDataset.php
+++ /dev/null
@@ -1,424 +0,0 @@
-model(),
- $userTable->table(),
- $userTable->getUserid()
- );
- $seed = $userTable->getGenerateKeyClosure();
- if (!empty($seed)) {
- $userMapper->withPrimaryKeySeedFunction($seed);
- }
-
- $propertyDefinition = $userTable->toArray();
-
- foreach ($propertyDefinition as $property => $map) {
- $userMapper->addFieldMapping(FieldMapping::create($property)
- ->withFieldName($map)
- ->withUpdateFunction($userTable->getClosureForUpdate($property))
- ->withSelectFunction($userTable->getClosureForSelect($property))
- );
- }
- $this->userRepository = new Repository($dbDriver, $userMapper);
-
- $propertiesMapper = new Mapper(
- UserPropertiesModel::class,
- $propertiesTable->table(),
- $propertiesTable->getId()
- );
- $propertiesMapper->addFieldMapping(FieldMapping::create('id')->withFieldName($propertiesTable->getId()));
- $propertiesMapper->addFieldMapping(FieldMapping::create('name')->withFieldName($propertiesTable->getName()));
- $propertiesMapper->addFieldMapping(FieldMapping::create('value')->withFieldName($propertiesTable->getValue()));
- $propertiesMapper->addFieldMapping(FieldMapping::create(UserDefinition::FIELD_USERID)
- ->withFieldName($propertiesTable->getUserid())
- ->withUpdateFunction($userTable->getClosureForUpdate(UserDefinition::FIELD_USERID))
- ->withSelectFunction($userTable->getClosureForSelect(UserDefinition::FIELD_USERID))
- );
- $this->propertiesRepository = new Repository($dbDriver, $propertiesMapper);
-
- $this->userTable = $userTable;
- $this->propertiesTable = $propertiesTable;
- }
-
- /**
- * Save the current UsersAnyDataset
- *
- * @param UserModel $model
- * @return UserModel
- * @throws UserExistsException
- * @throws OrmBeforeInvalidException
- * @throws OrmInvalidFieldsException
- * @throws Exception
- */
- public function save(UserModel $model): UserModel
- {
- $newUser = false;
- if (empty($model->getUserid())) {
- $this->canAddUser($model);
- $newUser = true;
- }
-
- $this->userRepository->setBeforeUpdate($this->userTable->getBeforeUpdate());
- $this->userRepository->setBeforeInsert($this->userTable->getBeforeInsert());
- $this->userRepository->save($model);
-
- foreach ($model->getProperties() as $property) {
- $property->setUserid($model->getUserid());
- $this->propertiesRepository->save($property);
- }
-
- if ($newUser) {
- $model = $this->getByEmail($model->getEmail());
- }
-
- if ($model === null) {
- throw new UserExistsException("User not found");
- }
-
- return $model;
- }
-
- /**
- * Get the users database information based on a filter.
- *
- * @param IteratorFilter|null $filter Filter to find user
- * @return UserModel[]
- */
- public function getIterator(IteratorFilter $filter = null): array
- {
- if (is_null($filter)) {
- $filter = new IteratorFilter();
- }
-
- $param = [];
- $formatter = new IteratorFilterSqlFormatter();
- $sql = $formatter->getFilter($filter->getRawFilters(), $param);
-
- $query = Query::getInstance()
- ->table($this->getUserDefinition()->table())
- ->where($sql, $param);
-
- return $this->userRepository->getByQuery($query);
- }
-
- /**
- * Get the user based on a filter.
- * Return Row if user was found; null, otherwise
- *
- * @param IteratorFilter $filter Filter to find user
- * @return UserModel|null
- */
- public function getUser(IteratorFilter $filter): UserModel|null
- {
- $result = $this->getIterator($filter);
- if (count($result) === 0) {
- return null;
- }
-
- $model = $result[0];
-
- $this->setPropertiesInUser($model);
-
- return $model;
- }
-
- /**
- * Remove the user based on his user login.
- *
- * @param string $login
- * @return bool
- * @throws Exception
- */
- public function removeByLoginField(string $login): bool
- {
- $user = $this->getByLoginField($login);
-
- if ($user !== null) {
- return $this->removeUserById($user->getUserid());
- }
-
- return false;
- }
-
- /**
- * Remove the user based on his user id.
- *
- * @param string|HexUuidLiteral|int $userid
- * @return bool
- * @throws Exception
- */
- public function removeUserById(string|HexUuidLiteral|int $userid): bool
- {
- $updateTableProperties = DeleteQuery::getInstance()
- ->table($this->getUserPropertiesDefinition()->table())
- ->where(
- "{$this->getUserPropertiesDefinition()->getUserid()} = :id",
- [
- "id" => $userid
- ]
- );
- $this->propertiesRepository->deleteByQuery($updateTableProperties);
-
- $this->userRepository->delete($userid);
-
- return true;
- }
-
- /**
- * Get the user based on his property.
- *
- * @param string $propertyName
- * @param string $value
- * @return array
- * @throws InvalidArgumentException
- * @throws ExceptionInvalidArgumentException
- */
- public function getUsersByProperty(string $propertyName, string $value): array
- {
- return $this->getUsersByPropertySet([$propertyName => $value]);
- }
-
- /**
- * Get the user based on his property and value. e.g. [ 'key' => 'value', 'key2' => 'value2' ].
- *
- * @param array $propertiesArray
- * @return array
- * @throws InvalidArgumentException
- * @throws ExceptionInvalidArgumentException
- */
- public function getUsersByPropertySet(array $propertiesArray): array
- {
- $query = Query::getInstance()
- ->field("u.*")
- ->table($this->getUserDefinition()->table(), "u");
-
- $count = 0;
- foreach ($propertiesArray as $propertyName => $value) {
- $count++;
- $query->join($this->getUserPropertiesDefinition()->table(), "p$count.{$this->getUserPropertiesDefinition()->getUserid()} = u.{$this->getUserDefinition()->getUserid()}", "p$count")
- ->where("p$count.{$this->getUserPropertiesDefinition()->getName()} = :name$count", ["name$count" => $propertyName])
- ->where("p$count.{$this->getUserPropertiesDefinition()->getValue()} = :value$count", ["value$count" => $value]);
- }
-
- return $this->userRepository->getByQuery($query);
- }
-
- /**
- * @param string|int|HexUuidLiteral $userId
- * @param string $propertyName
- * @param string|null $value
- * @return bool
- * @throws UserNotFoundException
- * @throws OrmBeforeInvalidException
- * @throws OrmInvalidFieldsException
- * @throws Exception
- */
- public function addProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value): bool
- {
- //anydataset.Row
- $user = $this->getById($userId);
- if (empty($user)) {
- return false;
- }
-
- if (!$this->hasProperty($userId, $propertyName, $value)) {
- $propertiesModel = new UserPropertiesModel($propertyName, $value);
- $propertiesModel->setUserid($userId);
- $this->propertiesRepository->save($propertiesModel);
- }
-
- return true;
- }
-
- /**
- * @throws UpdateConstraintException
- * @throws RepositoryReadOnlyException
- * @throws InvalidArgumentException
- * @throws OrmBeforeInvalidException
- * @throws ExceptionInvalidArgumentException
- * @throws OrmInvalidFieldsException
- */
- public function setProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value): bool
- {
- $query = Query::getInstance()
- ->table($this->getUserPropertiesDefinition()->table())
- ->where("{$this->getUserPropertiesDefinition()->getUserid()} = :id", ["id" => $userId])
- ->where("{$this->getUserPropertiesDefinition()->getName()} = :name", ["name" => $propertyName]);
-
- $userProperty = $this->propertiesRepository->getByQuery($query);
- if (empty($userProperty)) {
- $userProperty = new UserPropertiesModel($propertyName, $value);
- $userProperty->setUserid($userId);
- } else {
- $userProperty = $userProperty[0];
- $userProperty->setValue($value);
- }
-
- $this->propertiesRepository->save($userProperty);
-
- return true;
- }
-
- /**
- * Remove a specific site from user
- * Return True or false
- *
- * @param string|int|HexUuidLiteral $userId User Id
- * @param string $propertyName Property name
- * @param string|null $value Property value with a site
- * @return bool
- * @throws ExceptionInvalidArgumentException
- * @throws InvalidArgumentException
- * @throws RepositoryReadOnlyException
- */
- public function removeProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value = null): bool
- {
- $user = $this->getById($userId);
- if ($user !== null) {
-
- $updateable = DeleteQuery::getInstance()
- ->table($this->getUserPropertiesDefinition()->table())
- ->where("{$this->getUserPropertiesDefinition()->getUserid()} = :id", ["id" => $userId])
- ->where("{$this->getUserPropertiesDefinition()->getName()} = :name", ["name" => $propertyName]);
-
- if (!empty($value)) {
- $updateable->where("{$this->getUserPropertiesDefinition()->getValue()} = :value", ["value" => $value]);
- }
-
- $this->propertiesRepository->deleteByQuery($updateable);
-
- return true;
- }
-
- return false;
- }
-
- /**
- * Remove a specific site from all users
- * Return True or false
- *
- * @param string $propertyName Property name
- * @param string|null $value Property value with a site
- * @return void
- * @throws ExceptionInvalidArgumentException
- * @throws RepositoryReadOnlyException
- */
- public function removeAllProperties(string $propertyName, string|null $value = null): void
- {
- $updateable = DeleteQuery::getInstance()
- ->table($this->getUserPropertiesDefinition()->table())
- ->where("{$this->getUserPropertiesDefinition()->getName()} = :name", ["name" => $propertyName]);
-
- if (!empty($value)) {
- $updateable->where("{$this->getUserPropertiesDefinition()->getValue()} = :value", ["value" => $value]);
- }
-
- $this->propertiesRepository->deleteByQuery($updateable);
- }
-
- public function getProperty(string|HexUuidLiteral|int $userId, string $propertyName): array|string|UserPropertiesModel|null
- {
- $query = Query::getInstance()
- ->table($this->getUserPropertiesDefinition()->table())
- ->where("{$this->getUserPropertiesDefinition()->getUserid()} = :id", ['id' =>$userId])
- ->where("{$this->getUserPropertiesDefinition()->getName()} = :name", ['name' =>$propertyName]);
-
- $result = [];
- foreach ($this->propertiesRepository->getByQuery($query) as $model) {
- $result[] = $model->getValue();
- }
-
- if (count($result) === 0) {
- return null;
- }
-
- if (count($result) === 1) {
- return $result[0];
- }
-
- return $result;
- }
-
- /**
- * Return all property's fields from this user
- *
- * @param UserModel $userRow
- * @throws RepositoryReadOnlyException
- */
- protected function setPropertiesInUser(UserModel $userRow): void
- {
- $value = $this->propertiesRepository->getMapper()->getFieldMap(UserDefinition::FIELD_USERID)->getUpdateFunctionValue($userRow->getUserid(), $userRow, $this->propertiesRepository->getDbDriverWrite()->getDbHelper());
- $query = Query::getInstance()
- ->table($this->getUserPropertiesDefinition()->table())
- ->where("{$this->getUserPropertiesDefinition()->getUserid()} = :id", ['id' => $value]);
- $userRow->setProperties($this->propertiesRepository->getByQuery($query));
- }
-}
diff --git a/tests/CustomUserModel.php b/tests/CustomUserModel.php
new file mode 100644
index 0000000..58f1767
--- /dev/null
+++ b/tests/CustomUserModel.php
@@ -0,0 +1,41 @@
+setOtherfield($field);
+ }
+
+ public function getOtherfield()
+ {
+ return $this->otherfield;
+ }
+
+ public function setOtherfield($otherfield): void
+ {
+ $this->otherfield = $otherfield;
+ }
+}
diff --git a/tests/Fixture/MyUserPropertiesModel.php b/tests/Fixture/MyUserPropertiesModel.php
new file mode 100644
index 0000000..93d1bc8
--- /dev/null
+++ b/tests/Fixture/MyUserPropertiesModel.php
@@ -0,0 +1,23 @@
+isPasswordEncrypted($value)) {
+ return $value;
+ }
+
+ // Leave null
+ if (empty($value)) {
+ return null;
+ }
+
+ // Return the MD5 hash
+ return strtolower(md5($value));
+ }
+
+ #[\Override]
+ public function isPasswordEncrypted(mixed $password): bool
+ {
+ return is_string($password) && strlen($password) === 32 && ctype_xdigit($password);
+ }
+}
diff --git a/tests/Fixture/TestUniqueIdGenerator.php b/tests/Fixture/TestUniqueIdGenerator.php
new file mode 100644
index 0000000..19fa231
--- /dev/null
+++ b/tests/Fixture/TestUniqueIdGenerator.php
@@ -0,0 +1,23 @@
+prefix = $prefix;
+ }
+
+ #[Override]
+ public function processedValue(mixed $value, mixed $instance, ?DatabaseExecutor $executor = null): mixed
+ {
+ return $this->prefix . uniqid();
+ }
+}
diff --git a/tests/Fixture/UserModelMd5.php b/tests/Fixture/UserModelMd5.php
new file mode 100644
index 0000000..ed2ef98
--- /dev/null
+++ b/tests/Fixture/UserModelMd5.php
@@ -0,0 +1,41 @@
+ 0 // Allow repeated characters
];
- public function test__construct()
+ public function test__construct(): void
{
// Create Empty Password Definition
$passwordDefinition = new PasswordDefinition();
$this->assertEquals($this->defaultRules, $passwordDefinition->getRules());
}
- public function testSetRule()
+ public function testSetRule(): void
{
$passwordDefinition = new PasswordDefinition();
$passwordDefinition->setRule(PasswordDefinition::MINIMUM_CHARS, 10);
$this->assertEquals(10, $passwordDefinition->getRule(PasswordDefinition::MINIMUM_CHARS));
}
- public function testSetRuleInvalid()
+ public function testSetRuleInvalid(): void
{
$this->expectException(\InvalidArgumentException::class);
$passwordDefinition = new PasswordDefinition();
$passwordDefinition->setRule('invalid', 10);
}
- public function testGetRule()
+ public function testGetRule(): void
{
$passwordDefinition = new PasswordDefinition();
$this->assertEquals(8, $passwordDefinition->getRule(PasswordDefinition::MINIMUM_CHARS));
}
- public function testGetRuleInvalid()
+ public function testGetRuleInvalid(): void
{
$this->expectException(\InvalidArgumentException::class);
$passwordDefinition = new PasswordDefinition();
$passwordDefinition->getRule('invalid');
}
- public function testMatchPasswordMinimumChars()
+ public function testMatchPasswordMinimumChars(): void
{
$passwordDefinition = new PasswordDefinition([
PasswordDefinition::MINIMUM_CHARS => 8,
@@ -68,7 +68,7 @@ public function testMatchPasswordMinimumChars()
$this->assertEquals(PasswordDefinition::SUCCESS, $passwordDefinition->matchPassword('12345678'));
}
- public function testMatchPasswordUppercase()
+ public function testMatchPasswordUppercase(): void
{
$passwordDefinition = new PasswordDefinition([
PasswordDefinition::MINIMUM_CHARS => 8,
@@ -85,7 +85,7 @@ public function testMatchPasswordUppercase()
$this->assertEquals(PasswordDefinition::SUCCESS, $passwordDefinition->matchPassword('1234567BA'));
}
- public function testMatchPasswordLowercase()
+ public function testMatchPasswordLowercase(): void
{
$passwordDefinition = new PasswordDefinition([
PasswordDefinition::MINIMUM_CHARS => 8,
@@ -102,7 +102,7 @@ public function testMatchPasswordLowercase()
$this->assertEquals(PasswordDefinition::SUCCESS, $passwordDefinition->matchPassword('1234567ba'));
}
- public function testMatchPasswordSymbols()
+ public function testMatchPasswordSymbols(): void
{
$passwordDefinition = new PasswordDefinition([
PasswordDefinition::MINIMUM_CHARS => 8,
@@ -119,7 +119,7 @@ public function testMatchPasswordSymbols()
$this->assertEquals(PasswordDefinition::SUCCESS, $passwordDefinition->matchPassword('1234567!!'));
}
- public function testMatchPasswordNumbers()
+ public function testMatchPasswordNumbers(): void
{
$passwordDefinition = new PasswordDefinition([
PasswordDefinition::MINIMUM_CHARS => 8,
@@ -136,7 +136,7 @@ public function testMatchPasswordNumbers()
$this->assertEquals(PasswordDefinition::SUCCESS, $passwordDefinition->matchPassword('abcdef11'));
}
- public function testMatchPasswordWhitespace()
+ public function testMatchPasswordWhitespace(): void
{
$passwordDefinition = new PasswordDefinition([
PasswordDefinition::MINIMUM_CHARS => 8,
@@ -151,7 +151,7 @@ public function testMatchPasswordWhitespace()
$this->assertEquals(PasswordDefinition::FAIL_WHITESPACE, $passwordDefinition->matchPassword('1234 678'));
}
- public function testMatchPasswordSequential()
+ public function testMatchPasswordSequential(): void
{
$passwordDefinition = new PasswordDefinition([
PasswordDefinition::MINIMUM_CHARS => 8,
@@ -170,7 +170,7 @@ public function testMatchPasswordSequential()
$this->assertEquals(PasswordDefinition::SUCCESS, $passwordDefinition->matchPassword('diykdsn132'));
}
- public function testMatchCharsRepeated()
+ public function testMatchCharsRepeated(): void
{
$passwordDefinition = new PasswordDefinition([
PasswordDefinition::MINIMUM_CHARS => 8,
@@ -188,4 +188,76 @@ public function testMatchCharsRepeated()
$this->assertEquals(PasswordDefinition::FAIL_REPEATED, $passwordDefinition->matchPassword('oilalalapo')); // lalala is repeated
$this->assertEquals(PasswordDefinition::SUCCESS, $passwordDefinition->matchPassword('hay1d11oihsc'));
}
+
+ public function testGeneratePassword(): void
+ {
+ for ($i = 0; $i < 100; $i++) {
+ $passwordDefinition = new PasswordDefinition([
+ PasswordDefinition::MINIMUM_CHARS => 8,
+ PasswordDefinition::REQUIRE_UPPERCASE => 2, // Number of uppercase characters
+ PasswordDefinition::REQUIRE_LOWERCASE => 2, // Number of lowercase characters
+ PasswordDefinition::REQUIRE_SYMBOLS => 2, // Number of symbols
+ PasswordDefinition::REQUIRE_NUMBERS => 2, // Number of numbers
+ PasswordDefinition::ALLOW_WHITESPACE => 0, // Allow whitespace
+ PasswordDefinition::ALLOW_SEQUENTIAL => 0, // Allow sequential characters
+ PasswordDefinition::ALLOW_REPEATED => 0 // Allow repeated characters
+ ]);
+
+ $password = $passwordDefinition->generatePassword();
+ $this->assertEquals(PasswordDefinition::SUCCESS, $passwordDefinition->matchPassword($password));
+ $this->assertEquals(8, strlen($password));
+
+ $password = $passwordDefinition->generatePassword(2);
+ $this->assertEquals(PasswordDefinition::SUCCESS, $passwordDefinition->matchPassword($password));
+ $this->assertEquals(10, strlen($password));
+ }
+ }
+
+ public function testGeneratePassword2(): void
+ {
+ for ($i = 0; $i < 100; $i++) {
+ $passwordDefinition = new PasswordDefinition([
+ PasswordDefinition::MINIMUM_CHARS => 8,
+ PasswordDefinition::REQUIRE_UPPERCASE => 1, // Number of uppercase characters
+ PasswordDefinition::REQUIRE_LOWERCASE => 1, // Number of lowercase characters
+ PasswordDefinition::REQUIRE_SYMBOLS => 0, // Number of symbols
+ PasswordDefinition::REQUIRE_NUMBERS => 0, // Number of numbers
+ PasswordDefinition::ALLOW_WHITESPACE => 0, // Allow whitespace
+ PasswordDefinition::ALLOW_SEQUENTIAL => 0, // Allow sequential characters
+ PasswordDefinition::ALLOW_REPEATED => 0 // Allow repeated characters
+ ]);
+
+ $password = $passwordDefinition->generatePassword();
+ $this->assertEquals(PasswordDefinition::SUCCESS, $passwordDefinition->matchPassword($password));
+ $this->assertEquals(8, strlen($password));
+
+ $password = $passwordDefinition->generatePassword(2);
+ $this->assertEquals(PasswordDefinition::SUCCESS, $passwordDefinition->matchPassword($password));
+ $this->assertEquals(10, strlen($password));
+ }
+ }
+
+ public function testGeneratePasswordEmpty(): void
+ {
+ for ($i = 0; $i < 100; $i++) {
+ $passwordDefinition = new PasswordDefinition([
+ PasswordDefinition::MINIMUM_CHARS => 8,
+ PasswordDefinition::REQUIRE_UPPERCASE => 0, // Number of uppercase characters
+ PasswordDefinition::REQUIRE_LOWERCASE => 0, // Number of lowercase characters
+ PasswordDefinition::REQUIRE_SYMBOLS => 0, // Number of symbols
+ PasswordDefinition::REQUIRE_NUMBERS => 0, // Number of numbers
+ PasswordDefinition::ALLOW_WHITESPACE => 0, // Allow whitespace
+ PasswordDefinition::ALLOW_SEQUENTIAL => 0, // Allow sequential characters
+ PasswordDefinition::ALLOW_REPEATED => 0 // Allow repeated characters
+ ]);
+
+ $password = $passwordDefinition->generatePassword();
+ $this->assertEquals(PasswordDefinition::SUCCESS, $passwordDefinition->matchPassword($password));
+ $this->assertEquals(8, strlen($password));
+
+ $password = $passwordDefinition->generatePassword(2);
+ $this->assertEquals(PasswordDefinition::SUCCESS, $passwordDefinition->matchPassword($password));
+ $this->assertEquals(10, strlen($password));
+ }
+ }
}
\ No newline at end of file
diff --git a/tests/PasswordMd5MapperTest.php b/tests/PasswordMd5MapperTest.php
new file mode 100644
index 0000000..6257082
--- /dev/null
+++ b/tests/PasswordMd5MapperTest.php
@@ -0,0 +1,193 @@
+db = Factory::getDbInstance(self::CONNECTION_STRING);
+ $this->db->execute('create table users (
+ userid integer primary key autoincrement,
+ name varchar(45),
+ email varchar(200),
+ username varchar(20),
+ password varchar(40),
+ created_at datetime default (datetime(\'2017-12-04\')),
+ updated_at datetime,
+ deleted_at datetime,
+ role varchar(20));'
+ );
+
+ $this->db->execute('create table users_property (
+ id integer primary key autoincrement,
+ userid integer,
+ name varchar(45),
+ value varchar(45));'
+ );
+
+ // Create repositories and service with custom MD5 password mapper via UserModelMd5
+ $executor = DatabaseExecutor::using($this->db);
+ $usersRepository = new UsersRepository($executor, UserModelMd5::class);
+ $propertiesRepository = new UserPropertiesRepository($executor, UserPropertiesModel::class);
+ $this->service = new UsersService(
+ $usersRepository,
+ $propertiesRepository,
+ LoginField::Username
+ );
+ }
+
+ #[\Override]
+ public function tearDown(): void
+ {
+ $uri = new Uri(self::CONNECTION_STRING);
+ if (file_exists($uri->getPath())) {
+ unlink($uri->getPath());
+ }
+ $this->db = null;
+ $this->service = null;
+ }
+
+ public function testPasswordIsHashedWithMd5OnSave(): void
+ {
+ // Add a user with a plain text password
+ $plainPassword = 'mySecretPassword123';
+ $user = $this->service->addUser('John Doe', 'johndoe', 'john@example.com', $plainPassword);
+
+ // Verify the password was hashed with MD5
+ $expectedHash = md5($plainPassword);
+ $this->assertEquals($expectedHash, $user->getPassword());
+ $this->assertEquals(32, strlen($user->getPassword())); // MD5 is always 32 chars
+ $this->assertTrue(ctype_xdigit($user->getPassword())); // MD5 is hexadecimal
+ }
+
+ public function testPasswordIsNotRehashedIfAlreadyMd5(): void
+ {
+ // Create a user
+ $user = $this->service->addUser('Jane Doe', 'janedoe', 'jane@example.com', 'password123');
+ $originalHash = $user->getPassword();
+
+ // Update the user without changing password
+ $user->setName('Jane Smith');
+ $updatedUser = $this->service->save($user);
+
+ // Password hash should remain the same
+ $this->assertEquals($originalHash, $updatedUser->getPassword());
+ }
+
+ public function testPasswordIsHashedWhenUpdating(): void
+ {
+ // Create a user
+ $user = $this->service->addUser('Jane Doe', 'janedoe', 'jane@example.com', 'oldPassword');
+ $oldHash = $user->getPassword();
+
+ // Update the password with a new plain text password
+ $newPlainPassword = 'newPassword123';
+ $user->setPassword($newPlainPassword);
+ $updatedUser = $this->service->save($user);
+
+ // Verify the new password was hashed with MD5
+ $expectedNewHash = md5($newPlainPassword);
+ $this->assertEquals($expectedNewHash, $updatedUser->getPassword());
+
+ // Verify it's different from the old hash
+ $this->assertNotEquals($oldHash, $updatedUser->getPassword());
+
+ // Verify user can login with new password
+ $authenticatedUser = $this->service->isValidUser('janedoe', $newPlainPassword);
+ $this->assertNotNull($authenticatedUser);
+
+ // Verify user cannot login with old password
+ $authenticatedUserOld = $this->service->isValidUser('janedoe', 'oldPassword');
+ $this->assertNull($authenticatedUserOld);
+ }
+
+ public function testPasswordRemainsUnchangedWhenUpdatingOtherFields(): void
+ {
+ // Create a user
+ $originalPassword = 'myPassword123';
+ $user = $this->service->addUser('John Smith', 'johnsmith', 'john@example.com', $originalPassword);
+ $originalHash = $user->getPassword();
+
+ // Update other fields WITHOUT touching the password
+ $user->setName('John Updated');
+ $user->setEmail('johnupdated@example.com');
+ $user->setRole('admin');
+ $updatedUser = $this->service->save($user);
+
+ // Verify the password hash remained exactly the same
+ $this->assertEquals($originalHash, $updatedUser->getPassword());
+ $this->assertEquals(32, strlen($updatedUser->getPassword()));
+
+ // Verify other fields were updated
+ $this->assertEquals('John Updated', $updatedUser->getName());
+ $this->assertEquals('johnupdated@example.com', $updatedUser->getEmail());
+ $this->assertEquals('admin', $updatedUser->getRole());
+
+ // Verify user can still login with original password
+ $authenticatedUser = $this->service->isValidUser('johnsmith', $originalPassword);
+ $this->assertNotNull($authenticatedUser);
+ $this->assertEquals('John Updated', $authenticatedUser->getName());
+ }
+
+ public function testUserCanLoginWithMd5HashedPassword(): void
+ {
+ // Add a user
+ $plainPassword = 'testPassword456';
+ $this->service->addUser('Test User', 'testuser', 'test@example.com', $plainPassword);
+
+ // Verify user can login with the plain text password
+ $authenticatedUser = $this->service->isValidUser('testuser', $plainPassword);
+
+ $this->assertNotNull($authenticatedUser);
+ $this->assertEquals('Test User', $authenticatedUser->getName());
+ $this->assertEquals('testuser', $authenticatedUser->getUsername());
+ }
+
+ public function testUserCannotLoginWithWrongPassword(): void
+ {
+ // Add a user
+ $this->service->addUser('Test User', 'testuser', 'test@example.com', 'correctPassword');
+
+ // Try to login with wrong password
+ $authenticatedUser = $this->service->isValidUser('testuser', 'wrongPassword');
+
+ $this->assertNull($authenticatedUser);
+ }
+
+ public function testEmptyPasswordReturnsNull(): void
+ {
+ $mapper = new PasswordMd5Mapper();
+
+ $this->assertNull($mapper->processedValue('', null));
+ $this->assertNull($mapper->processedValue(null, null));
+ }
+
+ public function testExistingMd5HashIsNotRehashed(): void
+ {
+ $mapper = new PasswordMd5Mapper();
+ $existingHash = '5f4dcc3b5aa765d61d8327deb882cf99'; // MD5 of 'password'
+
+ $result = $mapper->processedValue($existingHash, null);
+
+ $this->assertEquals($existingHash, $result);
+ }
+}
diff --git a/tests/SessionContextTest.php b/tests/SessionContextTest.php
index 33b1067..f09f672 100644
--- a/tests/SessionContextTest.php
+++ b/tests/SessionContextTest.php
@@ -14,17 +14,19 @@ class SessionContextTest extends TestCase
*/
protected $object;
+ #[\Override]
public function setUp(): void
{
$this->object = new SessionContext(Factory::createSessionPool());
}
+ #[\Override]
public function tearDown(): void
{
$this->object = null;
}
- public function testUserContext()
+ public function testUserContext(): void
{
$this->assertFalse($this->object->isAuthenticated());
@@ -44,13 +46,13 @@ public function testUserContext()
$this->assertFalse($this->object->isAuthenticated());
}
- public function testUserContextNotActiveSession()
+ public function testUserContextNotActiveSession(): void
{
$this->expectException(\ByJG\Authenticate\Exception\NotAuthenticatedException::class);
$this->assertEmpty($this->object->getSessionData('property1'));
}
- public function testUserContextNotActive2Session()
+ public function testUserContextNotActive2Session(): void
{
$this->expectException(\ByJG\Authenticate\Exception\NotAuthenticatedException::class);
$this->object->setSessionData('property1', 'value');
diff --git a/tests/TestUsersBase.php b/tests/TestUsersBase.php
new file mode 100644
index 0000000..e58447d
--- /dev/null
+++ b/tests/TestUsersBase.php
@@ -0,0 +1,746 @@
+loginField) {
+ LoginField::Email => $forEmail,
+ LoginField::Username => $forUsername,
+ };
+ }
+
+ #[\Override]
+ public function setUp(): void
+ {
+ $this->__setUp(LoginField::Username);
+ }
+
+ /**
+ * @return void
+ */
+ abstract public function testAddUser();
+
+ public function testAddUserError(): void
+ {
+ $this->expectException(UserExistsException::class);
+ $this->object->addUser('some user with same username', 'user2', 'user2@gmail.com', 'mypassword');
+ }
+
+ public function testAddProperty(): void
+ {
+ // Check state
+ $user = $this->object->getById($this->prefix . '2');
+ $this->assertEmpty($user->get('city'));
+
+ // Add one property
+ $this->object->addProperty($this->prefix . '2', 'city', 'Rio de Janeiro');
+ $user = $this->object->getById($this->prefix . '2');
+ $this->assertEquals('Rio de Janeiro', $user->get('city'));
+
+ // Add another property (cannot change)
+ $this->object->addProperty($this->prefix . '2', 'city', 'Belo Horizonte');
+ $user = $this->object->getById($this->prefix . '2');
+ $this->assertEquals(['Rio de Janeiro', 'Belo Horizonte'], $user->get('city'));
+
+ // Get Property
+ $this->assertEquals(['Rio de Janeiro', 'Belo Horizonte'], $this->object->getProperty($this->prefix . '2', 'city'));
+
+ // Add another property
+ $this->object->addProperty($this->prefix . '2', 'state', 'RJ');
+ $user = $this->object->getById($this->prefix . '2');
+ $this->assertEquals('RJ', $user->get('state'));
+
+ // Remove Property
+ $this->object->removeProperty($this->prefix . '2', 'state', 'RJ');
+ $user = $this->object->getById($this->prefix . '2');
+ $this->assertEmpty($user->get('state'));
+
+ // Remove Property Again
+ $this->object->removeProperty($this->prefix . '2', 'city', 'Rio de Janeiro');
+ $this->assertEquals('Belo Horizonte', $this->object->getProperty($this->prefix . '2', 'city'));
+
+ }
+
+ public function testRemoveAllProperties(): void
+ {
+ // Add the properties
+ $this->object->addProperty($this->prefix . '2', 'city', 'Rio de Janeiro');
+ $this->object->addProperty($this->prefix . '2', 'city', 'Niteroi');
+ $this->object->addProperty($this->prefix . '2', 'state', 'RJ');
+ $user = $this->object->getById($this->prefix . '2');
+ $this->assertEquals(['Rio de Janeiro', 'Niteroi'], $user->get('city'));
+ $this->assertEquals('RJ', $user->get('state'));
+
+ // Add another properties
+ $this->object->addProperty($this->prefix . '1', 'city', 'Niteroi');
+ $this->object->addProperty($this->prefix . '1', 'state', 'BA');
+ $user = $this->object->getById($this->prefix . '1');
+ $this->assertEquals('Niteroi', $user->get('city'));
+ $this->assertEquals('BA', $user->get('state'));
+
+ // Remove Properties
+ $this->object->removeAllProperties('state');
+ $user = $this->object->getById($this->prefix . '2');
+ $this->assertEquals(['Rio de Janeiro', 'Niteroi'], $user->get('city'));
+ $this->assertEmpty($user->get('state'));
+ $user = $this->object->getById($this->prefix . '1');
+ $this->assertEquals('Niteroi', $user->get('city'));
+ $this->assertEmpty($user->get('state'));
+
+ // Remove Properties Again
+ $this->object->removeAllProperties('city', 'Niteroi');
+ $user = $this->object->getById($this->prefix . '2');
+ $this->assertEquals('Rio de Janeiro', $user->get('city'));
+ $this->assertEmpty($user->get('state'));
+ $user = $this->object->getById($this->prefix . '1');
+ $this->assertEmpty($user->get('city'));
+ $this->assertEmpty($user->get('state'));
+
+ }
+
+ public function testRemoveByLoginField(): void
+ {
+ $login = $this->__chooseValue('user1', 'user1@gmail.com');
+
+ $user = $this->object->getByLogin($login);
+ $this->assertNotNull($user);
+
+ $result = $this->object->removeByLogin($login);
+ $this->assertTrue($result);
+
+ $user = $this->object->getByLogin($login);
+ $this->assertNull($user);
+ }
+
+ public function testEditUser(): void
+ {
+ $login = $this->__chooseValue('user1', 'user1@gmail.com');
+
+ // Getting data
+ $user = $this->object->getByLogin($login);
+ $this->assertEquals('User 1', $user->getName());
+
+ // Change and Persist data
+ $user->setName('Other name');
+ $this->object->save($user);
+
+ // Check if data persists
+ $user = $this->object->getById($this->prefix . '1');
+ $this->assertEquals('Other name', $user->getName());
+ }
+
+ public function testIsValidUser(): void
+ {
+ $login = $this->__chooseValue('user3', 'user3@gmail.com');
+ $loginFalse = $this->__chooseValue('user3@gmail.com', 'user3');
+
+ // User Exists!
+ $user = $this->object->isValidUser($login, 'pwd3');
+ $this->assertEquals('User 3', $user->getName());
+
+ // User Does not Exists!
+ $user = $this->object->isValidUser($loginFalse, 'pwd5');
+ $this->assertNull($user);
+ }
+
+ public function testIsAdmin(): void
+ {
+ // Check user has no role initially
+ $user3 = $this->object->getById($this->prefix . '3');
+ $this->assertFalse($user3->hasRole('admin'));
+ $this->assertEquals("foobar", $user3->getRole());
+
+ // Set a role
+ $login = $this->__chooseValue('user3', 'user3@gmail.com');
+ $user = $this->object->getByLogin($login);
+ $user->setRole('admin');
+ $this->object->save($user);
+
+ // Check user has the role
+ $user3 = $this->object->getById($this->prefix . '3');
+ $this->assertTrue($user3->hasRole('admin'));
+ $this->assertTrue($user3->hasRole('ADMIN')); // Case insensitive
+ $this->assertFalse($user3->hasRole('user'));
+ $this->assertEquals('admin', $user3->getRole());
+
+ // Test different roles
+ $user->setRole('moderator');
+ $this->object->save($user);
+
+ $user3 = $this->object->getById($this->prefix . '3');
+ $this->assertFalse($user3->hasRole('admin'));
+ $this->assertTrue($user3->hasRole('moderator'));
+ $this->assertEquals('moderator', $user3->getRole());
+ }
+
+ protected function expectedToken(string $tokenData, string $login, int $userId): void
+ {
+ $loginCreated = $this->__chooseValue('user2', 'user2@gmail.com');
+
+ $jwtWrapper = new JwtWrapper('api.test.com', new JwtHashHmacSecret('12345678', false));
+
+ $userToken = $this->object->createAuthToken(
+ $loginCreated,
+ 'pwd2',
+ $jwtWrapper,
+ 1200,
+ ['userData'=>'userValue'],
+ ['tokenData'=>$tokenData]
+ );
+
+ $user = $this->object->getByLogin($login);
+
+ $dataFromToken = [
+ 'tokenData' => $tokenData,
+ 'userid' => $userId,
+ 'name' => $user->getName(),
+ ];
+
+ $tokenResult = $this->object->isValidToken($loginCreated, $jwtWrapper, $userToken->token);
+
+ // Compare data object
+ $this->assertEquals($dataFromToken, $tokenResult->data);
+
+ // Compare user fields (excluding timestamps which may differ)
+ $this->assertEquals($user->getUserid(), $tokenResult->user->getUserid());
+ $this->assertEquals($user->get("userData"), "userValue");
+ $this->assertEquals($user->get("userData"), $tokenResult->user->get("userData"));
+ }
+
+ /**
+ * @return void
+ */
+ abstract public function testCreateAuthToken();
+
+ public function testCreateAuthTokenReturnsUserToken(): void
+ {
+ $login = $this->__chooseValue('user2', 'user2@gmail.com');
+ $jwtWrapper = new JwtWrapper('api.test.com', new JwtHashHmacSecret('12345678', false));
+
+ $userToken = $this->object->createAuthToken(
+ $login,
+ 'pwd2',
+ $jwtWrapper,
+ 1200,
+ tokenUserFields: [
+ UserField::Username->value,
+ UserField::Email->value,
+ UserField::Role->value => "nobody"
+ ]
+ );
+
+ $this->assertInstanceOf(UserToken::class, $userToken);
+ $this->assertNotEmpty($userToken->token);
+ $this->assertInstanceOf(UserModel::class, $userToken->user);
+ $this->assertEquals(2, $userToken->user->getUserid());
+ $this->assertEquals('user2', $userToken->data["username"]);
+ $this->assertEquals('user2@gmail.com', $userToken->data["email"]);
+ $this->assertEquals('nobody', $userToken->data["role"]);
+ }
+
+ public function testCreateAuthTokenWithInvalidPassword(): void
+ {
+ $this->expectException(UserNotFoundException::class);
+ $login = $this->__chooseValue('user2', 'user2@gmail.com');
+ $jwtWrapper = new JwtWrapper('api.test.com', new JwtHashHmacSecret('12345678', false));
+
+ $this->object->createAuthToken(
+ $login,
+ 'wrongpassword',
+ $jwtWrapper,
+ 1200
+ );
+ }
+
+ public function testCreateAuthTokenWithUpdateUserInfo(): void
+ {
+ $login = $this->__chooseValue('user2', 'user2@gmail.com');
+ $jwtWrapper = new JwtWrapper('api.test.com', new JwtHashHmacSecret('12345678', false));
+
+ $userToken = $this->object->createAuthToken(
+ $login,
+ 'pwd2',
+ $jwtWrapper,
+ 1200,
+ ['last_login' => '2024-01-01 12:00:00']
+ );
+
+ $this->assertInstanceOf(UserToken::class, $userToken);
+ $this->assertEquals('2024-01-01 12:00:00', $userToken->user->get('last_login'));
+ }
+
+ public function testCreateAuthTokenWithCustomTokenData(): void
+ {
+ $login = $this->__chooseValue('user2', 'user2@gmail.com');
+ $jwtWrapper = new JwtWrapper('api.test.com', new JwtHashHmacSecret('12345678', false));
+
+ $userToken = $this->object->createAuthToken(
+ $login,
+ 'pwd2',
+ $jwtWrapper,
+ 1200,
+ [],
+ ['custom_field' => 'custom_value', 'another_field' => 123]
+ );
+
+ $this->assertInstanceOf(UserToken::class, $userToken);
+ $this->assertEquals('custom_value', $userToken->data['custom_field']);
+ $this->assertEquals(123, $userToken->data['another_field']);
+ }
+
+ public function testCreateAuthTokenIncludesDefaultUserFields(): void
+ {
+ $login = $this->__chooseValue('user2', 'user2@gmail.com');
+ $jwtWrapper = new JwtWrapper('api.test.com', new JwtHashHmacSecret('12345678', false));
+
+ $userToken = $this->object->createAuthToken(
+ $login,
+ 'pwd2',
+ $jwtWrapper,
+ 1200
+ );
+
+ $this->assertArrayHasKey('userid', $userToken->data);
+ $this->assertArrayHasKey('name', $userToken->data);
+ $this->assertArrayNotHasKey('role', $userToken->data); // Role isn't set because is null
+ $this->assertEquals(2, $userToken->data['userid']);
+ }
+
+ public function testCreateAuthTokenStoresTokenHash(): void
+ {
+ $login = $this->__chooseValue('user2', 'user2@gmail.com');
+ $jwtWrapper = new JwtWrapper('api.test.com', new JwtHashHmacSecret('12345678', false));
+
+ $userToken = $this->object->createAuthToken(
+ $login,
+ 'pwd2',
+ $jwtWrapper,
+ 1200
+ );
+
+ $expectedHash = sha1($userToken->token);
+ $this->assertEquals($expectedHash, $userToken->user->get('TOKEN_HASH'));
+ }
+
+ public function testCreateInsecureAuthTokenWithLoginString(): void
+ {
+ $login = $this->__chooseValue('user2', 'user2@gmail.com');
+ $jwtWrapper = new JwtWrapper('api.test.com', new JwtHashHmacSecret('12345678', false));
+
+ $userToken = $this->object->createInsecureAuthToken(
+ $login,
+ $jwtWrapper,
+ 1200
+ );
+
+ $this->assertInstanceOf(UserToken::class, $userToken);
+ $this->assertNotEmpty($userToken->token);
+ $this->assertInstanceOf(UserModel::class, $userToken->user);
+ $this->assertEquals(2, $userToken->user->getUserid());
+ }
+
+ public function testCreateInsecureAuthTokenWithUserModel(): void
+ {
+ $login = $this->__chooseValue('user2', 'user2@gmail.com');
+ $user = $this->object->getByLogin($login);
+ $jwtWrapper = new JwtWrapper('api.test.com', new JwtHashHmacSecret('12345678', false));
+
+ $userToken = $this->object->createInsecureAuthToken(
+ $user,
+ $jwtWrapper,
+ 1200
+ );
+
+ $this->assertInstanceOf(UserToken::class, $userToken);
+ $this->assertNotEmpty($userToken->token);
+ $this->assertEquals($user->getUserid(), $userToken->user->getUserid());
+ }
+
+ public function testCreateInsecureAuthTokenWithInvalidLogin(): void
+ {
+ $this->expectException(UserNotFoundException::class);
+ $jwtWrapper = new JwtWrapper('api.test.com', new JwtHashHmacSecret('12345678', false));
+
+ $this->object->createInsecureAuthToken(
+ 'nonexistent@example.com',
+ $jwtWrapper,
+ 1200
+ );
+ }
+
+ public function testCreateInsecureAuthTokenWithCustomFields(): void
+ {
+ $login = $this->__chooseValue('user2', 'user2@gmail.com');
+ $jwtWrapper = new JwtWrapper('api.test.com', new JwtHashHmacSecret('12345678', false));
+
+ $userToken = $this->object->createInsecureAuthToken(
+ $login,
+ $jwtWrapper,
+ 1200,
+ ['session_id' => 'abc123'],
+ ['ip_address' => '192.168.1.1']
+ );
+
+ $this->assertInstanceOf(UserToken::class, $userToken);
+ $this->assertEquals('abc123', $userToken->user->get('session_id'));
+ $this->assertEquals('192.168.1.1', $userToken->data['ip_address']);
+ }
+
+ public function testCreateInsecureAuthTokenStoresTokenHash(): void
+ {
+ $login = $this->__chooseValue('user2', 'user2@gmail.com');
+ $jwtWrapper = new JwtWrapper('api.test.com', new JwtHashHmacSecret('12345678', false));
+
+ $userToken = $this->object->createInsecureAuthToken(
+ $login,
+ $jwtWrapper,
+ 1200
+ );
+
+ $expectedHash = sha1($userToken->token);
+ $this->assertEquals($expectedHash, $userToken->user->get('TOKEN_HASH'));
+ }
+
+ public function testValidateTokenWithAnotherUser(): void
+ {
+ $this->expectException(NotAuthenticatedException::class);
+ $login = $this->__chooseValue('user2', 'user2@gmail.com');
+ $loginToFail = $this->__chooseValue('user1', 'user1@gmail.com');
+
+ $jwtWrapper = new JwtWrapper('api.test.com', new JwtHashHmacSecret('1234567'));
+ $userToken = $this->object->createAuthToken(
+ $login,
+ 'pwd2',
+ $jwtWrapper,
+ 1200,
+ ['userData'=>'userValue'],
+ ['tokenData'=>'tokenValue']
+ );
+
+ $this->object->isValidToken($loginToFail, $jwtWrapper, $userToken->token);
+ }
+
+ /**
+ * @return void
+ */
+ abstract public function testSaveAndSave();
+
+ public function testRemoveUserById(): void
+ {
+ $user = $this->object->getById($this->prefix . '1');
+ $this->assertNotNull($user);
+
+ $this->object->removeById($this->prefix . '1');
+
+ $user2 = $this->object->getById($this->prefix . '1');
+ $this->assertNull($user2);
+ }
+
+ public function testGetByUsername(): void
+ {
+ $user = $this->object->getByUsername('user2');
+
+ $this->assertEquals($this->prefix . '2', $user->getUserid());
+ $this->assertEquals('User 2', $user->getName());
+ $this->assertEquals('user2', $user->getUsername());
+ $this->assertEquals('user2@gmail.com', $user->getEmail());
+ $this->assertEquals('c88b5c841897dafe75cdd9f8ba98b32f007d6bc3', $user->getPassword());
+ }
+
+ public function testGetByUserProperty(): void
+ {
+ // Add property to user1
+ $user = $this->object->getById($this->prefix . '1');
+ $user->set('property1', 'somevalue');
+ $this->object->save($user);
+
+ // Add property to user2
+ $user = $this->object->getById($this->prefix . '2');
+ $user->set('property1', 'value1');
+ $user->set('property2', 'value2');
+ $this->object->save($user);
+
+ // Get user by property
+ $user = $this->object->getUsersByProperty('property1', 'value2');
+ $this->assertCount(0, $user);
+
+ // Get user by property
+ $user = $this->object->getUsersByProperty('property1', 'somevalue');
+ $this->assertCount(1, $user);
+ $this->assertEquals($this->prefix . '1', $user[0]->getUserid());
+
+ // Only one property is valid, so no results
+ $user = $this->object->getUsersByPropertySet(['property1'=>'xyz', 'property2'=>'value2']);
+ $this->assertCount(0, $user);
+
+ // Get user2 by property using method getUsersByPropertySet
+ $user = $this->object->getUsersByPropertySet(['property1'=>'value1', 'property2'=>'value2']);
+ $this->assertCount(1, $user);
+ $this->assertEquals($this->prefix . '2', $user[0]->getUserid());
+
+ }
+
+ public function testSetProperty(): void
+ {
+ $this->assertFalse($this->object->hasProperty($this->prefix . '1', 'propertySet'));
+ $this->object->setProperty($this->prefix . '1', 'propertySet', 'somevalue');
+ $this->assertTrue($this->object->hasProperty($this->prefix . '1', 'propertySet'));
+ $this->assertTrue($this->object->hasProperty($this->prefix . '1', 'propertySet', 'somevalue'));
+ $this->assertEquals('somevalue', $this->object->getProperty($this->prefix . '1', 'propertySet'));
+ }
+
+ public function testPasswordDefinitionValidOnSave(): void
+ {
+ // Create a password definition requiring uppercase, lowercase, and numbers
+ $passwordDef = new \ByJG\Authenticate\Definition\PasswordDefinition([
+ \ByJG\Authenticate\Definition\PasswordDefinition::MINIMUM_CHARS => 8,
+ \ByJG\Authenticate\Definition\PasswordDefinition::REQUIRE_UPPERCASE => 1,
+ \ByJG\Authenticate\Definition\PasswordDefinition::REQUIRE_LOWERCASE => 1,
+ \ByJG\Authenticate\Definition\PasswordDefinition::REQUIRE_NUMBERS => 1,
+ ]);
+
+ // Create a user model with valid password
+ $user = new \ByJG\Authenticate\Model\UserModel();
+ $user->setName('Test User');
+ $user->setUsername('testuser_pwd');
+ $user->setEmail('testpwd@example.com');
+ $user->setPassword('ValidPass8642'); // Valid: uppercase, lowercase, numbers, no sequential, 12 chars
+ $user->withPasswordDefinition($passwordDef);
+
+ // Should save successfully
+ $savedUser = $this->object->save($user);
+ $this->assertNotNull($savedUser->getUserid());
+ $this->assertEquals('Test User', $savedUser->getName());
+
+ // Verify user was saved
+ $retrievedUser = $this->object->getByUsername('testuser_pwd');
+ $this->assertNotNull($retrievedUser);
+ $this->assertEquals('testpwd@example.com', $retrievedUser->getEmail());
+ }
+
+ public function testPasswordDefinitionInvalidOnSave(): void
+ {
+ // Create a password definition requiring uppercase, lowercase, and numbers
+ $passwordDef = new \ByJG\Authenticate\Definition\PasswordDefinition([
+ \ByJG\Authenticate\Definition\PasswordDefinition::MINIMUM_CHARS => 8,
+ \ByJG\Authenticate\Definition\PasswordDefinition::REQUIRE_UPPERCASE => 1,
+ \ByJG\Authenticate\Definition\PasswordDefinition::REQUIRE_LOWERCASE => 1,
+ \ByJG\Authenticate\Definition\PasswordDefinition::REQUIRE_NUMBERS => 1,
+ ]);
+
+ // Create a user model with invalid password (no uppercase)
+ $user = new \ByJG\Authenticate\Model\UserModel();
+ $user->setName('Test User');
+ $user->setUsername('testuser_invalid');
+ $user->setEmail('invalid@example.com');
+ $user->withPasswordDefinition($passwordDef);
+
+ // Should throw exception because password doesn't match definition
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('Password does not match the password definition');
+ $user->setPassword('weakpass123'); // Invalid: no uppercase
+ }
+
+ public function testPasswordDefinitionValidOnUpdate(): void
+ {
+ // Get existing user
+ $user = $this->object->getById($this->prefix . '1');
+ $this->assertNotNull($user);
+
+ // Create a password definition
+ $passwordDef = new \ByJG\Authenticate\Definition\PasswordDefinition([
+ \ByJG\Authenticate\Definition\PasswordDefinition::MINIMUM_CHARS => 10,
+ \ByJG\Authenticate\Definition\PasswordDefinition::REQUIRE_UPPERCASE => 1,
+ \ByJG\Authenticate\Definition\PasswordDefinition::REQUIRE_LOWERCASE => 1,
+ \ByJG\Authenticate\Definition\PasswordDefinition::REQUIRE_NUMBERS => 2,
+ \ByJG\Authenticate\Definition\PasswordDefinition::REQUIRE_SYMBOLS => 1,
+ ]);
+
+ // Update password with valid value
+ $user->setPassword('StrongPass84!'); // Valid: uppercase, lowercase, 2 numbers, symbol, no sequential, 13 chars
+ $user->withPasswordDefinition($passwordDef);
+
+ // Should update successfully
+ $savedUser = $this->object->save($user);
+
+ // Verify password was updated (by checking authentication works with login field)
+ $login = $this->__chooseValue($user->getUsername(), $user->getEmail());
+ $validUser = $this->object->isValidUser($login, 'StrongPass84!');
+ $this->assertNotNull($validUser);
+ $this->assertEquals($user->getUserid(), $validUser->getUserid());
+ $this->assertEquals($user->getPassword(), $validUser->getPassword());
+ }
+
+ public function testPasswordDefinitionInvalidOnUpdate(): void
+ {
+ // Get existing user
+ $user = $this->object->getById($this->prefix . '2');
+ $this->assertNotNull($user);
+
+ // Create a strict password definition
+ $passwordDef = new \ByJG\Authenticate\Definition\PasswordDefinition([
+ \ByJG\Authenticate\Definition\PasswordDefinition::MINIMUM_CHARS => 12,
+ \ByJG\Authenticate\Definition\PasswordDefinition::REQUIRE_UPPERCASE => 2,
+ \ByJG\Authenticate\Definition\PasswordDefinition::REQUIRE_LOWERCASE => 2,
+ \ByJG\Authenticate\Definition\PasswordDefinition::REQUIRE_NUMBERS => 2,
+ \ByJG\Authenticate\Definition\PasswordDefinition::REQUIRE_SYMBOLS => 1,
+ ]);
+
+ $user->withPasswordDefinition($passwordDef);
+
+ // Should throw exception because password doesn't have 2 uppercase letters
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('Password does not match the password definition');
+ $user->setPassword('weak1!'); // Invalid: too short, not enough uppercase/lowercase/numbers
+ }
+
+ public function testPasswordDefinitionMultipleFailures(): void
+ {
+ // Create a strict password definition
+ $passwordDef = new \ByJG\Authenticate\Definition\PasswordDefinition([
+ \ByJG\Authenticate\Definition\PasswordDefinition::MINIMUM_CHARS => 12,
+ \ByJG\Authenticate\Definition\PasswordDefinition::REQUIRE_UPPERCASE => 1,
+ \ByJG\Authenticate\Definition\PasswordDefinition::REQUIRE_LOWERCASE => 1,
+ \ByJG\Authenticate\Definition\PasswordDefinition::REQUIRE_NUMBERS => 1,
+ \ByJG\Authenticate\Definition\PasswordDefinition::REQUIRE_SYMBOLS => 1,
+ \ByJG\Authenticate\Definition\PasswordDefinition::ALLOW_WHITESPACE => 0,
+ ]);
+
+ $user = new \ByJG\Authenticate\Model\UserModel();
+ $user->setName('Test User');
+ $user->setUsername('testuser_multi');
+ $user->setEmail('multi@example.com');
+ $user->withPasswordDefinition($passwordDef);
+
+ // Test multiple failure scenarios
+ try {
+ $user->setPassword('short'); // Fails: too short, no uppercase, no numbers, no symbols
+ $this->fail('Expected InvalidArgumentException was not thrown');
+ } catch (\InvalidArgumentException $e) {
+ $this->assertStringContainsString('Password does not match the password definition', $e->getMessage());
+ }
+
+ try {
+ $user->setPassword('with space 123A!'); // Fails: has whitespace
+ $this->fail('Expected InvalidArgumentException was not thrown');
+ } catch (\InvalidArgumentException $e) {
+ $this->assertStringContainsString('Password does not match the password definition', $e->getMessage());
+ }
+ }
+
+ public function testPasswordDefinitionViaServiceAddUser(): void
+ {
+ // Create a new service instance with password definition
+ $passwordDef = new \ByJG\Authenticate\Definition\PasswordDefinition([
+ \ByJG\Authenticate\Definition\PasswordDefinition::MINIMUM_CHARS => 10,
+ \ByJG\Authenticate\Definition\PasswordDefinition::REQUIRE_UPPERCASE => 1,
+ \ByJG\Authenticate\Definition\PasswordDefinition::REQUIRE_LOWERCASE => 1,
+ \ByJG\Authenticate\Definition\PasswordDefinition::REQUIRE_NUMBERS => 1,
+ \ByJG\Authenticate\Definition\PasswordDefinition::REQUIRE_SYMBOLS => 1,
+ ]);
+
+ // Get the repositories from existing service
+ $usersRepo = $this->object->getUsersRepository();
+ $propsRepo = $this->object->getPropertiesRepository();
+
+ // Create new service with password definition
+ $usersWithPwdDef = new \ByJG\Authenticate\Service\UsersService(
+ $usersRepo,
+ $propsRepo,
+ LoginField::Username,
+ $passwordDef
+ );
+
+ // Valid password should work
+ $user = $usersWithPwdDef->addUser(
+ 'Valid User',
+ 'validuser',
+ 'valid@example.com',
+ 'StrongPass8!' // Valid: 12 chars, uppercase, lowercase, number, symbol, no sequential
+ );
+ $this->assertNotNull($user->getUserid());
+ $this->assertEquals('Valid User', $user->getName());
+
+ // Invalid password should fail
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('Password does not match the password definition');
+ $usersWithPwdDef->addUser(
+ 'Invalid User',
+ 'invaliduser',
+ 'invalid@example.com',
+ 'weak' // Invalid: too short, no uppercase, no numbers, no symbols
+ );
+ }
+
+ public function testPasswordHashRemainsUnchangedOnSave(): void
+ {
+ // Create a user with a password
+ $originalPassword = 'mySecretPassword';
+ $user = $this->object->addUser('Test User', 'testuser', 'test@example.com', $originalPassword);
+
+ // Store the original password hash
+ $originalHash = $user->getPassword();
+ $this->assertEquals($originalHash, sha1($originalPassword));
+
+ // Verify the user can authenticate with the original password
+ $login = $this->__chooseValue('testuser', 'test@example.com');
+ $authenticatedUser = $this->object->isValidUser($login, $originalPassword);
+ $this->assertNotNull($authenticatedUser);
+ $this->assertEquals($user->getUserid(), $authenticatedUser->getUserid());
+
+ // Get the user from database
+ $retrievedUser = $this->object->getById($user->getUserid());
+ $this->assertEquals($originalHash, $retrievedUser->getPassword());
+
+ // Update other fields WITHOUT touching password
+ $retrievedUser->setName('Updated Name');
+ $retrievedUser->setEmail('updated@example.com');
+ $retrievedUser->setRole('moderator');
+
+ // Save the user
+ $updatedUser = $this->object->save($retrievedUser);
+
+ // Verify the password hash remained exactly the same
+ $this->assertEquals($originalHash, $updatedUser->getPassword());
+ $this->assertEquals('Updated Name', $updatedUser->getName());
+ $this->assertEquals('updated@example.com', $updatedUser->getEmail());
+ $this->assertEquals('moderator', $updatedUser->getRole());
+
+ // Verify user can still authenticate with original password
+ $login = $this->__chooseValue('testuser', 'updated@example.com');
+ $authenticatedUser = $this->object->isValidUser($login, $originalPassword);
+ $this->assertNotNull($authenticatedUser);
+ $this->assertEquals($user->getUserid(), $authenticatedUser->getUserid());
+
+ // Get user again from database to confirm persistence
+ $finalUser = $this->object->getById($user->getUserid());
+ $this->assertEquals($originalHash, $finalUser->getPassword());
+ $this->assertEquals('Updated Name', $finalUser->getName());
+ }
+}
diff --git a/tests/UserModelTest.php b/tests/UserModelTest.php
index a63b081..c5b0be9 100644
--- a/tests/UserModelTest.php
+++ b/tests/UserModelTest.php
@@ -15,17 +15,19 @@ class UserModelTest extends TestCase
*/
protected $object;
+ #[\Override]
public function setUp(): void
{
$this->object = new UserModel();
}
+ #[\Override]
public function tearDown(): void
{
$this->object = null;
}
- public function testUserModel()
+ public function testUserModel(): void
{
$this->object->setUserid("10");
$this->object->setName('John');
@@ -40,7 +42,7 @@ public function testUserModel()
$this->assertEquals('johnuser', $this->object->getUsername());
}
- public function testUserModelProperties()
+ public function testUserModelProperties(): void
{
$this->object->setUserid("10");
$this->object->setName('John');
@@ -64,7 +66,7 @@ public function testUserModelProperties()
], $this->object->getProperties());
}
- public function testPasswordDefinition()
+ public function testPasswordDefinition(): void
{
$this->object->withPasswordDefinition(new PasswordDefinition([
PasswordDefinition::MINIMUM_CHARS => 12,
@@ -77,12 +79,18 @@ public function testPasswordDefinition()
PasswordDefinition::ALLOW_REPEATED => 0 // Allow repeated characters
]));
- $this->assertEmpty($this->object->setPassword(null));
- $this->assertEmpty($this->object->setPassword(''));
- $this->assertEmpty($this->object->setPassword('!Ab18Uk*H2oU9NQ'));
+ // These passwords should be accepted (null, empty, and valid password)
+ $this->object->setPassword(null);
+ $this->assertNull($this->object->getPassword());
+
+ $this->object->setPassword('');
+ $this->assertEmpty($this->object->getPassword());
+
+ $this->object->setPassword('!Ab18Uk*H2oU9NQ');
+ $this->assertEquals('!Ab18Uk*H2oU9NQ', $this->object->getPassword());
}
- public function testPasswordDefinitionError()
+ public function testPasswordDefinitionError(): void
{
$this->expectException(InvalidArgumentException::class);
diff --git a/tests/UsersAnyDataset2ByEmailTest.php b/tests/UsersAnyDataset2ByEmailTest.php
deleted file mode 100644
index 19ba913..0000000
--- a/tests/UsersAnyDataset2ByEmailTest.php
+++ /dev/null
@@ -1,13 +0,0 @@
-__setUp(UserDefinition::LOGIN_IS_EMAIL);
- }
-}
diff --git a/tests/UsersAnyDataset2ByUsernameTest.php b/tests/UsersAnyDataset2ByUsernameTest.php
deleted file mode 100644
index 22d41e9..0000000
--- a/tests/UsersAnyDataset2ByUsernameTest.php
+++ /dev/null
@@ -1,57 +0,0 @@
-prefix = "user";
-
- $this->userDefinition = new UserDefinition(
- 'mytable',
- UserModel::class,
- $loginField,
- [
- UserDefinition::FIELD_USERID => 'myuserid',
- UserDefinition::FIELD_NAME => 'myname',
- UserDefinition::FIELD_EMAIL => 'myemail',
- UserDefinition::FIELD_USERNAME => 'myusername',
- UserDefinition::FIELD_PASSWORD => 'mypassword',
- UserDefinition::FIELD_CREATED => 'mycreated',
- UserDefinition::FIELD_ADMIN => 'myadmin'
- ]
- );
- $this->userDefinition->markPropertyAsReadOnly(UserDefinition::FIELD_CREATED);
-
- $this->propertyDefinition = new UserPropertiesDefinition(
- 'theirproperty',
- 'theirid',
- 'theirname',
- 'theirvalue',
- 'theiruserid'
- );
-
- $anydataset = new AnyDataset('php://memory');
- $this->object = new UsersAnyDataset(
- $anydataset,
- $this->userDefinition,
- $this->propertyDefinition
- );
- $this->object->addUser('User 1', 'user1', 'user1@gmail.com', 'pwd1');
- $this->object->addUser('User 2', 'user2', 'user2@gmail.com', 'pwd2');
- $this->object->addUser('User 3', 'user3', 'user3@gmail.com', 'pwd3');
- }
-
- public function setUp(): void
- {
- $this->__setUp(UserDefinition::LOGIN_IS_USERNAME);
- }
-}
diff --git a/tests/UsersAnyDatasetByEmailTest.php b/tests/UsersAnyDatasetByEmailTest.php
deleted file mode 100644
index 560ba99..0000000
--- a/tests/UsersAnyDatasetByEmailTest.php
+++ /dev/null
@@ -1,13 +0,0 @@
-__setUp(UserDefinition::LOGIN_IS_EMAIL);
- }
-}
diff --git a/tests/UsersAnyDatasetByUsernameTest.php b/tests/UsersAnyDatasetByUsernameTest.php
deleted file mode 100644
index c6b957c..0000000
--- a/tests/UsersAnyDatasetByUsernameTest.php
+++ /dev/null
@@ -1,350 +0,0 @@
-prefix = "user";
-
- $this->userDefinition = new UserDefinition('users', UserModel::class, $loginField);
- $this->propertyDefinition = new UserPropertiesDefinition();
-
- $anydataSet = new AnyDataset('php://memory');
- $this->object = new UsersAnyDataset(
- $anydataSet,
- $this->userDefinition,
- $this->propertyDefinition
- );
- $this->object->addUser('User 1', 'user1', 'user1@gmail.com', 'pwd1');
- $this->object->addUser('User 2', 'user2', 'user2@gmail.com', 'pwd2');
- $this->object->addUser('User 3', 'user3', 'user3@gmail.com', 'pwd3');
- }
-
- public function __chooseValue($forUsername, $forEmail)
- {
- $searchForList = [
- $this->userDefinition->getUsername() => $forUsername,
- $this->userDefinition->getEmail() => $forEmail,
- ];
- return $searchForList[$this->userDefinition->loginField()];
- }
-
- public function setUp(): void
- {
- $this->__setUp(UserDefinition::LOGIN_IS_USERNAME);
- }
-
- public function testAddUser()
- {
- $this->object->addUser('John Doe', 'john', 'johndoe@gmail.com', 'mypassword');
-
- $user = $this->object->getByLoginField($this->__chooseValue('john', 'johndoe@gmail.com'));
-
- $this->assertEquals('john', $user->getUserid());
- $this->assertEquals('John Doe', $user->getName());
- $this->assertEquals('john', $user->getUsername());
- $this->assertEquals('johndoe@gmail.com', $user->getEmail());
- $this->assertEquals('91dfd9ddb4198affc5c194cd8ce6d338fde470e2', $user->getPassword());
- }
-
- public function testAddUserError()
- {
- $this->expectException(UserExistsException::class);
- $this->object->addUser('some user with same username', 'user2', 'user2@gmail.com', 'mypassword');
- }
-
- public function testAddProperty()
- {
- // Check state
- $user = $this->object->getById($this->prefix . '2');
- $this->assertEmpty($user->get('city'));
-
- // Add one property
- $this->object->addProperty($this->prefix . '2', 'city', 'Rio de Janeiro');
- $user = $this->object->getById($this->prefix . '2');
- $this->assertEquals('Rio de Janeiro', $user->get('city'));
-
- // Add another property (cannot change)
- $this->object->addProperty($this->prefix . '2', 'city', 'Belo Horizonte');
- $user = $this->object->getById($this->prefix . '2');
- $this->assertEquals(['Rio de Janeiro', 'Belo Horizonte'], $user->get('city'));
-
- // Get Property
- $this->assertEquals(['Rio de Janeiro', 'Belo Horizonte'], $this->object->getProperty($this->prefix . '2', 'city'));
-
- // Add another property
- $this->object->addProperty($this->prefix . '2', 'state', 'RJ');
- $user = $this->object->getById($this->prefix . '2');
- $this->assertEquals('RJ', $user->get('state'));
-
- // Remove Property
- $this->object->removeProperty($this->prefix . '2', 'state', 'RJ');
- $user = $this->object->getById($this->prefix . '2');
- $this->assertEmpty($user->get('state'));
-
- // Remove Property Again
- $this->object->removeProperty($this->prefix . '2', 'city', 'Rio de Janeiro');
- $this->assertEquals('Belo Horizonte', $this->object->getProperty($this->prefix . '2', 'city'));
-
- }
-
- public function testRemoveAllProperties()
- {
- // Add the properties
- $this->object->addProperty($this->prefix . '2', 'city', 'Rio de Janeiro');
- $this->object->addProperty($this->prefix . '2', 'city', 'Niteroi');
- $this->object->addProperty($this->prefix . '2', 'state', 'RJ');
- $user = $this->object->getById($this->prefix . '2');
- $this->assertEquals(['Rio de Janeiro', 'Niteroi'], $user->get('city'));
- $this->assertEquals('RJ', $user->get('state'));
-
- // Add another properties
- $this->object->addProperty($this->prefix . '1', 'city', 'Niteroi');
- $this->object->addProperty($this->prefix . '1', 'state', 'BA');
- $user = $this->object->getById($this->prefix . '1');
- $this->assertEquals('Niteroi', $user->get('city'));
- $this->assertEquals('BA', $user->get('state'));
-
- // Remove Properties
- $this->object->removeAllProperties('state');
- $user = $this->object->getById($this->prefix . '2');
- $this->assertEquals(['Rio de Janeiro', 'Niteroi'], $user->get('city'));
- $this->assertEmpty($user->get('state'));
- $user = $this->object->getById($this->prefix . '1');
- $this->assertEquals('Niteroi', $user->get('city'));
- $this->assertEmpty($user->get('state'));
-
- // Remove Properties Again
- $this->object->removeAllProperties('city', 'Niteroi');
- $user = $this->object->getById($this->prefix . '2');
- $this->assertEquals('Rio de Janeiro', $user->get('city'));
- $this->assertEmpty($user->get('state'));
- $user = $this->object->getById($this->prefix . '1');
- $this->assertEmpty($user->get('city'));
- $this->assertEmpty($user->get('state'));
-
- }
-
- public function testRemoveByLoginField()
- {
- $login = $this->__chooseValue('user1', 'user1@gmail.com');
-
- $user = $this->object->getByLoginField($login);
- $this->assertNotNull($user);
-
- $result = $this->object->removeByLoginField($login);
- $this->assertTrue($result);
-
- $user = $this->object->getByLoginField($login);
- $this->assertNull($user);
- }
-
- public function testEditUser()
- {
- $login = $this->__chooseValue('user1', 'user1@gmail.com');
-
- // Getting data
- $user = $this->object->getByLoginField($login);
- $this->assertEquals('User 1', $user->getName());
-
- // Change and Persist data
- $user->setName('Other name');
- $this->object->save($user);
-
- // Check if data persists
- $user = $this->object->getById($this->prefix . '1');
- $this->assertEquals('Other name', $user->getName());
- }
-
- public function testIsValidUser()
- {
- $login = $this->__chooseValue('user3', 'user3@gmail.com');
- $loginFalse = $this->__chooseValue('user3@gmail.com', 'user3');
-
- // User Exists!
- $user = $this->object->isValidUser($login, 'pwd3');
- $this->assertEquals('User 3', $user->getName());
-
- // User Does not Exists!
- $user = $this->object->isValidUser($loginFalse, 'pwd5');
- $this->assertNull($user);
- }
-
- public function testIsAdmin()
- {
- // Check is Admin
- $this->assertFalse($this->object->isAdmin($this->prefix . '3'));
-
- // Set the Admin Flag
- $login = $this->__chooseValue('user3', 'user3@gmail.com');
- $user = $this->object->getByLoginField($login);
- $user->setAdmin('Y');
- $this->object->save($user);
-
- // Check is Admin
- $this->assertTrue($this->object->isAdmin($this->prefix . '3'));
- }
-
- protected function expectedToken($tokenData, $login, $userId)
- {
- $loginCreated = $this->__chooseValue('user2', 'user2@gmail.com');
-
- $jwtWrapper = new JwtWrapper('api.test.com', new JwtHashHmacSecret('12345678', false));
-
- $token = $this->object->createAuthToken(
- $loginCreated,
- 'pwd2',
- $jwtWrapper,
- 1200,
- ['userData'=>'userValue'],
- ['tokenData'=>$tokenData]
- );
-
- $user = $this->object->getByLoginField($login);
-
- $dataFromToken = new \stdClass();
- $dataFromToken->tokenData = $tokenData;
- $dataFromToken->login = $loginCreated;
- $dataFromToken->userid = $userId;
-
- $this->assertEquals(
- [
- 'user' => $user,
- 'data' => $dataFromToken
- ],
- $this->object->isValidToken($loginCreated, $jwtWrapper, $token)
- );
- }
-
- public function testCreateAuthToken()
- {
- $login = $this->__chooseValue('user2', 'user2@gmail.com');
-
- $this->expectedToken('tokenValue', $login, 'user2');
- }
-
- public function testValidateTokenWithAnotherUser()
- {
- $this->expectException(NotAuthenticatedException::class);
- $login = $this->__chooseValue('user2', 'user2@gmail.com');
- $loginToFail = $this->__chooseValue('user1', 'user1@gmail.com');
-
- $jwtWrapper = new JwtWrapper('api.test.com', new JwtHashHmacSecret('1234567'));
- $token = $this->object->createAuthToken(
- $login,
- 'pwd2',
- $jwtWrapper,
- 1200,
- ['userData'=>'userValue'],
- ['tokenData'=>'tokenValue']
- );
-
- $this->object->isValidToken($loginToFail, $jwtWrapper, $token);
- }
-
- public function testSaveAndSave()
- {
- $user = $this->object->getById('user1');
- $this->object->save($user);
-
- $user2 = $this->object->getById('user1');
-
- $this->assertEquals($user, $user2);
- }
-
- public function testRemoveUserById()
- {
- $user = $this->object->getById($this->prefix . '1');
- $this->assertNotNull($user);
-
- $this->object->removeUserById($this->prefix . '1');
-
- $user2 = $this->object->getById($this->prefix . '1');
- $this->assertNull($user2);
- }
-
- public function testGetByUsername()
- {
- $user = $this->object->getByUsername('user2');
-
- $this->assertEquals($this->prefix . '2', $user->getUserid());
- $this->assertEquals('User 2', $user->getName());
- $this->assertEquals('user2', $user->getUsername());
- $this->assertEquals('user2@gmail.com', $user->getEmail());
- $this->assertEquals('c88b5c841897dafe75cdd9f8ba98b32f007d6bc3', $user->getPassword());
- }
-
- public function testGetByUserProperty()
- {
- // Add property to user1
- $user = $this->object->getById($this->prefix . '1');
- $user->set('property1', 'somevalue');
- $this->object->save($user);
-
- // Add property to user2
- $user = $this->object->getById($this->prefix . '2');
- $user->set('property1', 'value1');
- $user->set('property2', 'value2');
- $this->object->save($user);
-
- // Get user by property
- $user = $this->object->getUsersByProperty('property1', 'value2');
- $this->assertCount(0, $user);
-
- // Get user by property
- $user = $this->object->getUsersByProperty('property1', 'somevalue');
- $this->assertCount(1, $user);
- $this->assertEquals($this->prefix . '1', $user[0]->getUserid());
-
- // Only one property is valid, so no results
- $user = $this->object->getUsersByPropertySet(['property1'=>'xyz', 'property2'=>'value2']);
- $this->assertCount(0, $user);
-
- // Get user2 by property using method getUsersByPropertySet
- $user = $this->object->getUsersByPropertySet(['property1'=>'value1', 'property2'=>'value2']);
- $this->assertCount(1, $user);
- $this->assertEquals($this->prefix . '2', $user[0]->getUserid());
-
- }
-
- public function testSetProperty()
- {
- $this->assertFalse($this->object->hasProperty($this->prefix . '1', 'propertySet'));
- $this->object->setProperty($this->prefix . '1', 'propertySet', 'somevalue');
- $this->assertTrue($this->object->hasProperty($this->prefix . '1', 'propertySet'));
- $this->assertTrue($this->object->hasProperty($this->prefix . '1', 'propertySet', 'somevalue'));
- $this->assertEquals('somevalue', $this->object->getProperty($this->prefix . '1', 'propertySet'));
- }
-}
diff --git a/tests/UsersDBDataset2ByEmailTest.php b/tests/UsersDBDataset2ByEmailTest.php
index 694f024..4c3e692 100644
--- a/tests/UsersDBDataset2ByEmailTest.php
+++ b/tests/UsersDBDataset2ByEmailTest.php
@@ -2,12 +2,13 @@
namespace Tests;
-use ByJG\Authenticate\Definition\UserDefinition;
+use ByJG\Authenticate\Enum\LoginField;
-class UsersDBDataset2ByEmailTest extends UsersDBDatasetByUsernameTest
+class UsersDBDataset2ByEmailTest extends UsersDBDataset2ByUserNameTestUsersBase
{
+ #[\Override]
public function setUp(): void
{
- $this->__setUp(UserDefinition::LOGIN_IS_EMAIL);
+ $this->__setUp(LoginField::Email);
}
}
diff --git a/tests/UsersDBDataset2ByUserNameTest.php b/tests/UsersDBDataset2ByUserNameTest.php
deleted file mode 100644
index 382179c..0000000
--- a/tests/UsersDBDataset2ByUserNameTest.php
+++ /dev/null
@@ -1,75 +0,0 @@
-prefix = "";
-
- $this->db = Factory::getDbRelationalInstance(self::CONNECTION_STRING);
- $this->db->execute('create table mytable (
- myuserid integer primary key autoincrement,
- myname varchar(45),
- myemail varchar(200),
- myusername varchar(20),
- mypassword varchar(40),
- mycreated datetime default (datetime(\'2017-12-04\')),
- myadmin char(1));'
- );
-
- $this->db->execute('create table theirproperty (
- theirid integer primary key autoincrement,
- theiruserid integer,
- theirname varchar(45),
- theirvalue varchar(45));'
- );
-
- $this->userDefinition = new UserDefinition(
- 'mytable',
- UserModel::class,
- $loginField,
- [
- UserDefinition::FIELD_USERID => 'myuserid',
- UserDefinition::FIELD_NAME => 'myname',
- UserDefinition::FIELD_EMAIL => 'myemail',
- UserDefinition::FIELD_USERNAME => 'myusername',
- UserDefinition::FIELD_PASSWORD => 'mypassword',
- UserDefinition::FIELD_CREATED => 'mycreated',
- UserDefinition::FIELD_ADMIN => 'myadmin'
- ]
- );
- $this->userDefinition->markPropertyAsReadOnly(UserDefinition::FIELD_CREATED);
-
- $this->propertyDefinition = new UserPropertiesDefinition('theirproperty', 'theirid', 'theirname', 'theirvalue', 'theiruserid');
-
- $this->object = new UsersDBDataset(
- $this->db,
- $this->userDefinition,
- $this->propertyDefinition
- );
-
- $this->object->addUser('User 1', 'user1', 'user1@gmail.com', 'pwd1');
- $this->object->addUser('User 2', 'user2', 'user2@gmail.com', 'pwd2');
- $this->object->addUser('User 3', 'user3', 'user3@gmail.com', 'pwd3');
- }
-
- public function setUp(): void
- {
- $this->__setUp(UserDefinition::LOGIN_IS_USERNAME);
- }
-}
diff --git a/tests/UsersDBDataset2ByUserNameTestUsersBase.php b/tests/UsersDBDataset2ByUserNameTestUsersBase.php
new file mode 100644
index 0000000..25c9687
--- /dev/null
+++ b/tests/UsersDBDataset2ByUserNameTestUsersBase.php
@@ -0,0 +1,132 @@
+prefix = "";
+ $this->loginField = $loginField;
+
+ $this->db = Factory::getDbRelationalInstance(self::CONNECTION_STRING);
+ $this->db->execute('create table mytable (
+ myuserid integer primary key autoincrement,
+ myname varchar(45),
+ myemail varchar(200),
+ myusername varchar(20),
+ mypassword varchar(40),
+ mycreated_at datetime default (datetime(\'2017-12-04\')),
+ myupdated_at datetime,
+ mydeleted_at datetime,
+ myrole varchar(20));'
+ );
+
+ $this->db->execute('create table theirproperty (
+ theirid integer primary key autoincrement,
+ theiruserid integer,
+ theirname varchar(45),
+ theirvalue varchar(45));'
+ );
+
+ $executor = DatabaseExecutor::using($this->db);
+ $usersRepository = new UsersRepository($executor, CustomUserModel::class);
+ $propertiesRepository = new UserPropertiesRepository($executor, CustomUserPropertiesModel::class);
+ $this->object = new UsersService(
+ $usersRepository,
+ $propertiesRepository,
+ $loginField
+ );
+
+ $this->object->addUser('User 1', 'user1', 'user1@gmail.com', 'pwd1', "role3");
+ $this->object->addUser('User 2', 'user2', 'user2@gmail.com', 'pwd2');
+ $this->object->addUser('User 3', 'user3', 'user3@gmail.com', 'pwd3', "foobar");
+ }
+
+ #[\Override]
+ public function setUp(): void
+ {
+ $this->__setUp(LoginField::Username);
+ }
+
+ #[\Override]
+ public function tearDown(): void
+ {
+ $uri = new Uri(self::CONNECTION_STRING);
+ unlink($uri->getPath());
+ $this->object = null;
+ }
+
+ /**
+ * @return void
+ */
+ #[\Override]
+ public function testAddUser()
+ {
+ $this->object->addUser('John Doe', 'john', 'johndoe@gmail.com', 'mypassword');
+
+ $login = $this->__chooseValue('john', 'johndoe@gmail.com');
+
+ $user = $this->object->getByLogin($login);
+ $this->assertEquals('4', $user->getUserid());
+ $this->assertEquals('John Doe', $user->getName());
+ $this->assertEquals('john', $user->getUsername());
+ $this->assertEquals('johndoe@gmail.com', $user->getEmail());
+ $this->assertEquals('91dfd9ddb4198affc5c194cd8ce6d338fde470e2', $user->getPassword());
+ $this->assertEquals('', $user->getRole());
+ $this->assertNotNull($user->getCreatedAt());
+ $this->assertMatchesRegularExpression('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $user->getCreatedAt());
+
+ // Setting role
+ $user->setRole('admin');
+ $this->object->save($user);
+
+ $user2 = $this->object->getByLogin($login);
+ $this->assertEquals('admin', $user2->getRole());
+ }
+
+ /**
+ * @return void
+ */
+ #[\Override]
+ public function testCreateAuthToken()
+ {
+ $login = $this->__chooseValue('user2', 'user2@gmail.com');
+ $this->expectedToken('tokenValue', $login, 2);
+ }
+
+ /**
+ * @return void
+ */
+ #[\Override]
+ public function testSaveAndSave()
+ {
+ $user = $this->object->getById("1");
+ $this->object->save($user);
+
+ $user2 = $this->object->getById("1");
+
+ // Compare all fields except updated_at which changes on each save
+ $this->assertEquals($user->getUserid(), $user2->getUserid());
+ $this->assertEquals($user->getName(), $user2->getName());
+ $this->assertEquals($user->getEmail(), $user2->getEmail());
+ $this->assertEquals($user->getUsername(), $user2->getUsername());
+ $this->assertEquals($user->getPassword(), $user2->getPassword());
+ $this->assertEquals($user->getRole(), $user2->getRole());
+ $this->assertEquals($user->getCreatedAt(), $user2->getCreatedAt());
+ // updated_at is expected to be different due to the save operation
+ }
+}
diff --git a/tests/UsersDBDatasetByEmailTest.php b/tests/UsersDBDatasetByEmailTest.php
index edaa39b..2f166e0 100644
--- a/tests/UsersDBDatasetByEmailTest.php
+++ b/tests/UsersDBDatasetByEmailTest.php
@@ -2,12 +2,13 @@
namespace Tests;
-use ByJG\Authenticate\Definition\UserDefinition;
+use ByJG\Authenticate\Enum\LoginField;
-class UsersDBDatasetByEmailTest extends UsersAnyDatasetByUsernameTest
+class UsersDBDatasetByEmailTest extends UsersDBDatasetByUsernameTestUsersBase
{
+ #[\Override]
public function setUp(): void
{
- $this->__setUp(UserDefinition::LOGIN_IS_EMAIL);
+ $this->__setUp(LoginField::Email);
}
}
diff --git a/tests/UsersDBDatasetByUsernameTest.php b/tests/UsersDBDatasetByUsernameTest.php
deleted file mode 100644
index 52df602..0000000
--- a/tests/UsersDBDatasetByUsernameTest.php
+++ /dev/null
@@ -1,165 +0,0 @@
-prefix = "";
-
- $this->db = Factory::getDbInstance(self::CONNECTION_STRING);
- $this->db->execute('create table users (
- userid integer primary key autoincrement,
- name varchar(45),
- email varchar(200),
- username varchar(20),
- password varchar(40),
- created datetime default (datetime(\'2017-12-04\')),
- admin char(1));'
- );
-
- $this->db->execute('create table users_property (
- id integer primary key autoincrement,
- userid integer,
- name varchar(45),
- value varchar(45));'
- );
-
- $this->userDefinition = new UserDefinition('users', UserModel::class, $loginField);
- $this->userDefinition->markPropertyAsReadOnly(UserDefinition::FIELD_CREATED);
- $this->propertyDefinition = new UserPropertiesDefinition();
- $this->object = new UsersDBDataset(
- $this->db,
- $this->userDefinition,
- $this->propertyDefinition
- );
-
- $user = $this->object->addUser('User 1', 'user1', 'user1@gmail.com', 'pwd1');
- $this->assertEquals(1, $user->getUserid());
- $this->assertEquals('User 1', $user->getName());
- $this->assertEquals('user1', $user->getUsername());
- $this->assertEquals('a63d4b132a9a1d3430f9ae507825f572449e0d17', $user->getPassword());
- $this->assertEquals('no', $user->getAdmin());
- $this->assertEquals('2017-12-04 00:00:00', $user->getCreated());
-
- $this->object->addUser('User 2', 'user2', 'user2@gmail.com', 'pwd2');
- $this->object->addUser('User 3', 'user3', 'user3@gmail.com', 'pwd3');
- }
-
- public function setUp(): void
- {
- $this->__setUp(UserDefinition::LOGIN_IS_USERNAME);
- }
-
- public function tearDown(): void
- {
- $uri = new Uri(self::CONNECTION_STRING);
- unlink($uri->getPath());
- $this->object = null;
- $this->userDefinition = null;
- $this->propertyDefinition = null;
- }
-
- public function testAddUser()
- {
- $this->object->addUser('John Doe', 'john', 'johndoe@gmail.com', 'mypassword');
-
- $login = $this->__chooseValue('john', 'johndoe@gmail.com');
-
- $user = $this->object->getByLoginField($login);
- $this->assertEquals('4', $user->getUserid());
- $this->assertEquals('John Doe', $user->getName());
- $this->assertEquals('john', $user->getUsername());
- $this->assertEquals('johndoe@gmail.com', $user->getEmail());
- $this->assertEquals('91dfd9ddb4198affc5c194cd8ce6d338fde470e2', $user->getPassword());
- $this->assertEquals('no', $user->getAdmin());
- $this->assertEquals('2017-12-04 00:00:00', $user->getCreated());
-
- // Setting as Admin
- $user->setAdmin('y');
- $this->object->save($user);
-
- $user2 = $this->object->getByLoginField($login);
- $this->assertEquals('y', $user2->getAdmin());
- }
-
- public function testCreateAuthToken()
- {
- $login = $this->__chooseValue('user2', 'user2@gmail.com');
- $this->expectedToken('tokenValue', $login, 2);
- }
-
- public function testWithUpdateValue()
- {
- // For Update Definitions
- $this->userDefinition->defineClosureForUpdate(UserDefinition::FIELD_NAME, function ($value, $instance) {
- return '[' . $value . ']';
- });
- $this->userDefinition->defineClosureForUpdate(UserDefinition::FIELD_USERNAME, function ($value, $instance) {
- return ']' . $value . '[';
- });
- $this->userDefinition->defineClosureForUpdate(UserDefinition::FIELD_EMAIL, function ($value, $instance) {
- return '-' . $value . '-';
- });
- $this->userDefinition->defineClosureForUpdate(UserDefinition::FIELD_PASSWORD, function ($value, $instance) {
- return "@" . $value . "@";
- });
- $this->userDefinition->markPropertyAsReadOnly(UserDefinition::FIELD_CREATED);
-
- // For Select Definitions
- $this->userDefinition->defineClosureForSelect(UserDefinition::FIELD_NAME, function ($value, $instance) {
- return '(' . $value . ')';
- });
- $this->userDefinition->defineClosureForSelect(UserDefinition::FIELD_USERNAME, function ($value, $instance) {
- return ')' . $value . '(';
- });
- $this->userDefinition->defineClosureForSelect(UserDefinition::FIELD_EMAIL, function ($value, $instance) {
- return '#' . $value . '#';
- });
- $this->userDefinition->defineClosureForSelect(UserDefinition::FIELD_PASSWORD, function ($value, $instance) {
- return '%'. $value . '%';
- });
-
- // Test it!
- $newObject = new UsersDBDataset(
- $this->db,
- $this->userDefinition,
- $this->propertyDefinition
- );
-
- $newObject->addUser('User 4', 'user4', 'user4@gmail.com', 'pwd4');
-
- $login = $this->__chooseValue(']user4[', '-user4@gmail.com-');
-
- $user = $newObject->getByLoginField($login);
- $this->assertEquals('4', $user->getUserid());
- $this->assertEquals('([User 4])', $user->getName());
- $this->assertEquals(')]user4[(', $user->getUsername());
- $this->assertEquals('#-user4@gmail.com-#', $user->getEmail());
- $this->assertEquals('%@pwd4@%', $user->getPassword());
- $this->assertEquals('2017-12-04 00:00:00', $user->getCreated());
- }
-
- public function testSaveAndSave()
- {
- $user = $this->object->getById("1");
- $this->object->save($user);
-
- $user2 = $this->object->getById("1");
-
- $this->assertEquals($user, $user2);
- }
-}
\ No newline at end of file
diff --git a/tests/UsersDBDatasetByUsernameTestUsersBase.php b/tests/UsersDBDatasetByUsernameTestUsersBase.php
new file mode 100644
index 0000000..4a5b266
--- /dev/null
+++ b/tests/UsersDBDatasetByUsernameTestUsersBase.php
@@ -0,0 +1,159 @@
+prefix = "";
+ $this->loginField = $loginField;
+
+ $this->db = Factory::getDbInstance(self::CONNECTION_STRING);
+ $this->db->execute('create table users (
+ userid integer primary key autoincrement,
+ name varchar(45),
+ email varchar(200),
+ username varchar(20),
+ password varchar(40),
+ created_at datetime default (datetime(\'2017-12-04\')),
+ updated_at datetime,
+ deleted_at datetime,
+ role varchar(20));'
+ );
+
+ $this->db->execute('create table users_property (
+ id integer primary key autoincrement,
+ userid integer,
+ name varchar(45),
+ value varchar(45));'
+ );
+
+ $executor = DatabaseExecutor::using($this->db);
+ $usersRepository = new UsersRepository($executor, UserModel::class);
+ $propertiesRepository = new UserPropertiesRepository($executor, UserPropertiesModel::class);
+ $this->object = new UsersService(
+ $usersRepository,
+ $propertiesRepository,
+ $loginField
+ );
+
+ $user = $this->object->addUser('User 1', 'user1', 'user1@gmail.com', 'pwd1');
+ $this->assertEquals(1, $user->getUserid());
+ $this->assertEquals('User 1', $user->getName());
+ $this->assertEquals('user1', $user->getUsername());
+ $this->assertEquals('a63d4b132a9a1d3430f9ae507825f572449e0d17', $user->getPassword());
+ $this->assertEquals('', $user->getRole());
+ $this->assertNotNull($user->getCreatedAt());
+ $this->assertMatchesRegularExpression('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $user->getCreatedAt());
+
+ $this->object->addUser('User 2', 'user2', 'user2@gmail.com', 'pwd2');
+ $this->object->addUser('User 3', 'user3', 'user3@gmail.com', 'pwd3', 'foobar');
+ }
+
+ #[\Override]
+ public function setUp(): void
+ {
+ $this->__setUp(LoginField::Username);
+ }
+
+ #[\Override]
+ public function tearDown(): void
+ {
+ $uri = new Uri(self::CONNECTION_STRING);
+ unlink($uri->getPath());
+ $this->object = null;
+ }
+
+ /**
+ * @return void
+ */
+ #[\Override]
+ public function testAddUser()
+ {
+ $this->object->addUser('John Doe', 'john', 'johndoe@gmail.com', 'mypassword');
+
+ $login = $this->__chooseValue('john', 'johndoe@gmail.com');
+
+ $user = $this->object->getByLogin($login);
+ $this->assertEquals('4', $user->getUserid());
+ $this->assertEquals('John Doe', $user->getName());
+ $this->assertEquals('john', $user->getUsername());
+ $this->assertEquals('johndoe@gmail.com', $user->getEmail());
+ $this->assertEquals('91dfd9ddb4198affc5c194cd8ce6d338fde470e2', $user->getPassword());
+ $this->assertEquals('', $user->getRole());
+ $this->assertNotNull($user->getCreatedAt());
+ $this->assertMatchesRegularExpression('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $user->getCreatedAt());
+
+ // Setting role
+ $user->setRole('admin');
+ $this->object->save($user);
+
+ $user2 = $this->object->getByLogin($login);
+ $this->assertEquals('admin', $user2->getRole());
+ }
+
+ /**
+ * @return void
+ */
+ #[\Override]
+ public function testCreateAuthToken()
+ {
+ $login = $this->__chooseValue('user2', 'user2@gmail.com');
+ $this->expectedToken('tokenValue', $login, 2);
+ }
+
+ /**
+ * @return void
+ */
+ // TODO: This test is currently disabled because the new architecture uses
+ // compile-time attributes instead of runtime mapper definitions.
+ // To achieve custom mappers, users should create a custom UserModel subclass
+ // with different mapper classes in the FieldAttribute annotations.
+ /*
+ public function testWithUpdateValue()
+ {
+ // This test was checking the old UserDefinition runtime mapper customization.
+ // In the new architecture, mappers are defined in the model's #[FieldAttribute]
+ // annotations at compile time, not modified at runtime.
+ }
+ */
+
+ /**
+ * @return void
+ */
+ #[\Override]
+ public function testSaveAndSave()
+ {
+ $user = $this->object->getById("1");
+ $this->object->save($user);
+
+ $user2 = $this->object->getById("1");
+
+ // Compare all fields except updated_at which changes on each save
+ $this->assertEquals($user->getUserid(), $user2->getUserid());
+ $this->assertEquals($user->getName(), $user2->getName());
+ $this->assertEquals($user->getEmail(), $user2->getEmail());
+ $this->assertEquals($user->getUsername(), $user2->getUsername());
+ $this->assertEquals($user->getPassword(), $user2->getPassword());
+ $this->assertEquals($user->getRole(), $user2->getRole());
+ $this->assertEquals($user->getCreatedAt(), $user2->getCreatedAt());
+ // updated_at is expected to be different due to the save operation
+ }
+}
\ No newline at end of file
diff --git a/tests/UsersDBDatasetDefinitionTest.php b/tests/UsersDBDatasetDefinitionTest.php
index a568842..e3479d5 100644
--- a/tests/UsersDBDatasetDefinitionTest.php
+++ b/tests/UsersDBDatasetDefinitionTest.php
@@ -3,40 +3,23 @@
namespace Tests;
use ByJG\AnyDataset\Core\Exception\DatabaseException;
+use ByJG\AnyDataset\Db\DatabaseExecutor;
use ByJG\AnyDataset\Db\Factory;
-use ByJG\Authenticate\Definition\UserDefinition;
-use ByJG\Authenticate\Definition\UserPropertiesDefinition;
+use ByJG\Authenticate\Enum\LoginField;
use ByJG\Authenticate\Exception\UserExistsException;
-use ByJG\Authenticate\Model\UserModel;
-use ByJG\Authenticate\UsersDBDataset;
+use ByJG\Authenticate\Repository\UserPropertiesRepository;
+use ByJG\Authenticate\Repository\UsersRepository;
+use ByJG\Authenticate\Service\UsersService;
use ByJG\MicroOrm\Exception\OrmBeforeInvalidException;
use ByJG\MicroOrm\Exception\OrmInvalidFieldsException;
use ByJG\MicroOrm\Exception\OrmModelInvalidException;
-use Exception;
+use ByJG\Util\Uri;
+use Override;
use ReflectionException;
+use Tests\Fixture\MyUserModel;
+use Tests\Fixture\MyUserPropertiesModel;
-class MyUserModel extends UserModel
-{
- protected $otherfield;
-
- public function __construct($name = "", $email = "", $username = "", $password = "", $admin = "no", $field = "")
- {
- parent::__construct($name, $email, $username, $password, $admin);
- $this->setOtherfield($field);
- }
-
- public function getOtherfield()
- {
- return $this->otherfield;
- }
-
- public function setOtherfield($otherfield)
- {
- $this->otherfield = $otherfield;
- }
-}
-
-class UsersDBDatasetDefinitionTest extends UsersDBDatasetByUsernameTest
+class UsersDBDatasetDefinitionTest extends UsersDBDatasetByUsernameTestUsersBase
{
protected $db;
@@ -48,61 +31,50 @@ class UsersDBDatasetDefinitionTest extends UsersDBDatasetByUsernameTest
* @throws UserExistsException
* @throws ReflectionException
*/
+ #[Override]
public function __setUp($loginField)
{
$this->prefix = "";
+ $this->loginField = $loginField;
$this->db = Factory::getDbInstance(self::CONNECTION_STRING);
$this->db->execute('create table mytable (
- myuserid integer primary key autoincrement,
- myname varchar(45),
- myemail varchar(200),
- myusername varchar(20),
- mypassword varchar(40),
- myotherfield varchar(40),
- mycreated datetime default (datetime(\'2017-12-04\')),
- myadmin char(1));'
+ myuserid integer primary key autoincrement,
+ myname varchar(45),
+ myemail varchar(200),
+ myusername varchar(20),
+ mypassword varchar(40),
+ myotherfield varchar(40),
+ mycreated_at datetime default (datetime(\'2017-12-04\')),
+ myupdated_at datetime,
+ mydeleted_at datetime,
+ myrole varchar(20));'
);
$this->db->execute('create table theirproperty (
- theirid integer primary key autoincrement,
- theiruserid integer,
- theirname varchar(45),
+ theirid integer primary key autoincrement,
+ theiruserid integer,
+ theirname varchar(45),
theirvalue varchar(45));'
);
- $this->userDefinition = new UserDefinition(
- 'mytable',
- MyUserModel::class,
- $loginField,
- [
- UserDefinition::FIELD_USERID => 'myuserid',
- UserDefinition::FIELD_NAME => 'myname',
- UserDefinition::FIELD_EMAIL => 'myemail',
- UserDefinition::FIELD_USERNAME => 'myusername',
- UserDefinition::FIELD_PASSWORD => 'mypassword',
- UserDefinition::FIELD_CREATED => 'mycreated',
- UserDefinition::FIELD_ADMIN => 'myadmin',
- 'otherfield' => 'myotherfield'
- ]
- );
-
- $this->propertyDefinition = new UserPropertiesDefinition('theirproperty', 'theirid', 'theirname', 'theirvalue', 'theiruserid');
-
- $this->object = new UsersDBDataset(
- $this->db,
- $this->userDefinition,
- $this->propertyDefinition
+ $executor = DatabaseExecutor::using($this->db);
+ $usersRepository = new UsersRepository($executor, MyUserModel::class);
+ $propertiesRepository = new UserPropertiesRepository($executor, MyUserPropertiesModel::class);
+ $this->object = new UsersService(
+ $usersRepository,
+ $propertiesRepository,
+ $loginField
);
$this->object->save(
- new MyUserModel('User 1', 'user1@gmail.com', 'user1', 'pwd1', 'no', 'other 1')
+ new MyUserModel('User 1', 'user1@gmail.com', 'user1', 'pwd1', '', 'other 1')
);
$this->object->save(
- new MyUserModel('User 2', 'user2@gmail.com', 'user2', 'pwd2', 'no', 'other 2')
+ new MyUserModel('User 2', 'user2@gmail.com', 'user2', 'pwd2', '', 'other 2')
);
$this->object->save(
- new MyUserModel('User 3', 'user3@gmail.com', 'user3', 'pwd3', 'no', 'other 3')
+ new MyUserModel('User 3', 'user3@gmail.com', 'user3', 'pwd3', 'foobar', 'other 3')
);
}
@@ -113,102 +85,83 @@ public function __setUp($loginField)
* @throws ReflectionException
* @throws UserExistsException
*/
+ #[Override]
public function setUp(): void
{
- $this->__setUp(UserDefinition::LOGIN_IS_USERNAME);
+ $this->__setUp(LoginField::Username);
+ }
+
+ #[\Override]
+ public function tearDown(): void
+ {
+ $uri = new Uri(self::CONNECTION_STRING);
+ unlink($uri->getPath());
+ $this->object = null;
}
/**
* @throws UserExistsException
* @throws DatabaseException
* @throws \ByJG\Serializer\Exception\InvalidArgumentException
+ *
+ * @return void
*/
+ #[Override]
public function testAddUser()
{
- $this->object->save(new MyUserModel('John Doe', 'johndoe@gmail.com', 'john', 'mypassword', 'no', 'other john'));
+ $this->object->save(new MyUserModel('John Doe', 'johndoe@gmail.com', 'john', 'mypassword', '', 'other john'));
$login = $this->__chooseValue('john', 'johndoe@gmail.com');
- $user = $this->object->getByLoginField($login);
+ $user = $this->object->getByLogin($login);
$this->assertEquals('4', $user->getUserid());
$this->assertEquals('John Doe', $user->getName());
$this->assertEquals('john', $user->getUsername());
$this->assertEquals('johndoe@gmail.com', $user->getEmail());
$this->assertEquals('91dfd9ddb4198affc5c194cd8ce6d338fde470e2', $user->getPassword());
- $this->assertEquals('no', $user->getAdmin());
+ $this->assertEquals('', $user->getRole());
/** @psalm-suppress UndefinedMethod Check UserModel::__call */
$this->assertEquals('other john', $user->getOtherfield());
- $this->assertEquals('', $user->getCreated()); // There is no default action for it
+ $this->assertNotNull($user->getCreatedAt());
+ $this->assertMatchesRegularExpression('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $user->getCreatedAt());
- // Setting as Admin
- $user->setAdmin('y');
+ // Setting role
+ $user->setRole('admin');
$this->object->save($user);
- $user2 = $this->object->getByLoginField($login);
- $this->assertEquals('y', $user2->getAdmin());
+ $user2 = $this->object->getByLogin($login);
+ $this->assertEquals('admin', $user2->getRole());
}
- /**
- * @throws Exception
- */
+ // TODO: These tests are currently disabled because the new architecture uses
+ // compile-time attributes instead of runtime mapper definitions.
+ // To achieve custom mappers, users should create a custom UserModel subclass
+ // with different mapper classes in the FieldAttribute annotations.
+ /*
public function testWithUpdateValue()
{
- // For Update Definitions
- $this->userDefinition->defineClosureForUpdate(UserDefinition::FIELD_NAME, function ($value, $instance) {
- return '[' . $value . ']';
- });
- $this->userDefinition->defineClosureForUpdate(UserDefinition::FIELD_USERNAME, function ($value, $instance) {
- return ']' . $value . '[';
- });
- $this->userDefinition->defineClosureForUpdate(UserDefinition::FIELD_EMAIL, function ($value, $instance) {
- return '-' . $value . '-';
- });
- $this->userDefinition->defineClosureForUpdate(UserDefinition::FIELD_PASSWORD, function ($value, $instance) {
- return "@" . $value . "@";
- });
- $this->userDefinition->markPropertyAsReadOnly(UserDefinition::FIELD_CREATED);
- $this->userDefinition->defineClosureForUpdate('otherfield', function ($value, $instance) {
- return "*" . $value . "*";
- });
-
- // For Select Definitions
- $this->userDefinition->defineClosureForSelect(UserDefinition::FIELD_NAME, function ($value, $instance) {
- return '(' . $value . ')';
- });
- $this->userDefinition->defineClosureForSelect(UserDefinition::FIELD_USERNAME, function ($value, $instance) {
- return ')' . $value . '(';
- });
- $this->userDefinition->defineClosureForSelect(UserDefinition::FIELD_EMAIL, function ($value, $instance) {
- return '#' . $value . '#';
- });
- $this->userDefinition->defineClosureForSelect(UserDefinition::FIELD_PASSWORD, function ($value, $instance) {
- return '%' . $value . '%';
- });
- $this->userDefinition->defineClosureForSelect('otherfield', function ($value, $instance) {
- return ']' . $value . '[';
- });
-
- // Test it!
- $newObject = new UsersDBDataset(
- $this->db,
- $this->userDefinition,
- $this->propertyDefinition
- );
+ // This test was checking the old UserDefinition runtime mapper customization.
+ // In the new architecture, mappers are defined in the model's #[FieldAttribute]
+ // annotations at compile time, not modified at runtime.
+ }
- $newObject->save(
- new MyUserModel('User 4', 'user4@gmail.com', 'user4', 'pwd4', 'no', 'other john')
- );
+ public function testDefineGenerateKeyWithInterface()
+ {
+ // This test was checking custom key generation via UserDefinition.
+ // In the new architecture, custom key generation should be done via
+ // MicroOrm's FieldAttribute primaryKey with a custom generator.
+ }
- $login = $this->__chooseValue(']user4[', '-user4@gmail.com-');
+ public function testDefineGenerateKeyWithString()
+ {
+ // This test was checking custom key generation via UserDefinition.
+ // In the new architecture, custom key generation should be done via
+ // MicroOrm's FieldAttribute primaryKey with a custom generator.
+ }
- $user = $newObject->getByLoginField($login);
- $this->assertEquals('4', $user->getUserid());
- $this->assertEquals('([User 4])', $user->getName());
- $this->assertEquals(')]user4[(', $user->getUsername());
- $this->assertEquals('#-user4@gmail.com-#', $user->getEmail());
- $this->assertEquals('%@pwd4@%', $user->getPassword());
- /** @psalm-suppress UndefinedMethod Check UserModel::__call */
- $this->assertEquals(']*other john*[', $user->getOtherfield());
- $this->assertEquals('2017-12-04 00:00:00', $user->getCreated());
+ public function testDefineGenerateKeyClosureThrowsException()
+ {
+ // UserDefinition has been removed in the new architecture.
}
+ */
}