From 6c7aad2cb82bc7b5fe15072390a09e004bc5c442 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Sat, 1 Nov 2025 14:24:47 -0400 Subject: [PATCH 01/40] Add password generation feature; update Psalm configuration and dependencies; annotate overridden methods --- .github/workflows/phpunit.yml | 3 +- .idea/runConfigurations/Psalm.xml | 5 -- composer.json | 14 +++-- psalm.xml | 1 + src/Definition/PasswordDefinition.php | 75 ++++++++++++++++++++++++ src/SessionContext.php | 6 ++ src/UsersAnyDataset.php | 10 ++++ src/UsersBase.php | 21 +++++++ src/UsersDBDataset.php | 11 ++++ tests/PasswordDefinitionTest.php | 72 +++++++++++++++++++++++ tests/SessionContextTest.php | 2 + tests/UserModelTest.php | 2 + tests/UsersAnyDataset2ByEmailTest.php | 1 + tests/UsersAnyDataset2ByUsernameTest.php | 2 + tests/UsersAnyDatasetByEmailTest.php | 1 + tests/UsersAnyDatasetByUsernameTest.php | 1 + tests/UsersDBDataset2ByEmailTest.php | 1 + tests/UsersDBDataset2ByUserNameTest.php | 2 + tests/UsersDBDatasetByEmailTest.php | 1 + tests/UsersDBDatasetByUsernameTest.php | 6 ++ tests/UsersDBDatasetDefinitionTest.php | 4 ++ 21 files changed, 230 insertions(+), 11 deletions(-) delete mode 100644 .idea/runConfigurations/Psalm.xml diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index d5322bf..1474292 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -33,5 +33,6 @@ jobs: with: folder: php project: ${{ github.event.repository.name }} - secrets: inherit + secrets: + DOC_TOKEN: ${{ secrets.DOC_TOKEN }} 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/composer.json b/composer.json index ea629b9..ced7781 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.1 <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" + "vimeo/psalm": "^5.9|^6.13" + }, + "scripts": { + "test": "vendor/bin/phpunit", + "psalm": "vendor/bin/psalm" }, "license": "MIT" } 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..ad27ece 100644 --- a/src/Definition/PasswordDefinition.php +++ b/src/Definition/PasswordDefinition.php @@ -113,6 +113,81 @@ public function matchPassword(string $password): int return $result; } + 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 = $this->rules[self::MINIMUM_CHARS] + $extendSize; + $totalChars = 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; + } } \ No newline at end of file diff --git a/src/SessionContext.php b/src/SessionContext.php index fe7a115..5cbf846 100644 --- a/src/SessionContext.php +++ b/src/SessionContext.php @@ -39,6 +39,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 +53,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 +65,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 +87,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 +114,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 +142,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 index 64a253b..2cda813 100644 --- a/src/UsersAnyDataset.php +++ b/src/UsersAnyDataset.php @@ -65,6 +65,7 @@ public function __construct( * @throws UserExistsException * @throws FileException */ + #[\Override] public function save(UserModel $model): UserModel { $new = true; @@ -103,6 +104,7 @@ public function save(UserModel $model): UserModel * @return UserModel|null * @throws InvalidArgumentException */ + #[\Override] public function getUser(IteratorFilter $filter): UserModel|null { $iterator = $this->anyDataSet->getIterator($filter); @@ -123,6 +125,7 @@ public function getUser(IteratorFilter $filter): UserModel|null * @throws FileException * @throws InvalidArgumentException */ + #[\Override] public function removeByLoginField(string $login): bool { //anydataset.Row @@ -154,6 +157,7 @@ public function getIterator(IteratorFilter $filter = null): IteratorInterface /** * @throws InvalidArgumentException */ + #[\Override] public function getUsersByProperty(string $propertyName, string $value): array { return $this->getUsersByPropertySet([$propertyName => $value]); @@ -162,6 +166,7 @@ public function getUsersByProperty(string $propertyName, string $value): array /** * @throws InvalidArgumentException */ + #[\Override] public function getUsersByPropertySet(array $propertiesArray): array { $filter = new IteratorFilter(); @@ -186,6 +191,7 @@ public function getUsersByPropertySet(array $propertiesArray): array * @throws UserExistsException * @throws UserNotFoundException */ + #[\Override] public function addProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value): bool { //anydataset.Row @@ -207,6 +213,7 @@ public function addProperty(string|HexUuidLiteral|int $userId, string $propertyN * @throws UserExistsException * @throws FileException */ + #[\Override] public function setProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value): bool { $user = $this->getById($userId); @@ -228,6 +235,7 @@ public function setProperty(string|HexUuidLiteral|int $userId, string $propertyN * @throws InvalidArgumentException * @throws UserExistsException */ + #[\Override] public function removeProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value = null): bool { $user = $this->getById($userId); @@ -258,6 +266,7 @@ public function removeProperty(string|HexUuidLiteral|int $userId, string $proper * @throws InvalidArgumentException * @throws UserExistsException */ + #[\Override] public function removeAllProperties(string $propertyName, string|null $value = null): void { $iterator = $this->getIterator(null); @@ -300,6 +309,7 @@ private function createUserModel(Row $row): UserModel * @return bool * @throws InvalidArgumentException */ + #[\Override] public function removeUserById(string|HexUuidLiteral|int $userid): bool { $iteratorFilter = new IteratorFilter(); diff --git a/src/UsersBase.php b/src/UsersBase.php index e8ed42d..c82b850 100644 --- a/src/UsersBase.php +++ b/src/UsersBase.php @@ -36,6 +36,7 @@ abstract class UsersBase implements UsersInterface /** * @return UserDefinition */ + #[\Override] public function getUserDefinition(): UserDefinition { if ($this->userTable === null) { @@ -47,6 +48,7 @@ public function getUserDefinition(): UserDefinition /** * @return UserPropertiesDefinition */ + #[\Override] public function getUserPropertiesDefinition(): UserPropertiesDefinition { if ($this->propertiesTable === null) { @@ -60,6 +62,7 @@ public function getUserPropertiesDefinition(): UserPropertiesDefinition * * @param UserModel $model */ + #[\Override] abstract public function save(UserModel $model): UserModel; /** @@ -71,6 +74,7 @@ abstract public function save(UserModel $model): UserModel; * @param string $password * @return UserModel */ + #[\Override] public function addUser(string $name, string $userName, string $email, string $password): UserModel { $model = $this->getUserDefinition()->modelInstance(); @@ -88,6 +92,7 @@ public function addUser(string $name, string $userName, string $email, string $p * @throws UserExistsException * @throws InvalidArgumentException */ + #[\Override] public function canAddUser(UserModel $model): bool { if ($this->getByEmail($model->getEmail()) !== null) { @@ -109,6 +114,7 @@ public function canAddUser(UserModel $model): bool * @param IteratorFilter $filter Filter to find user * @return UserModel|null * */ + #[\Override] abstract public function getUser(IteratorFilter $filter): UserModel|null; /** @@ -119,6 +125,7 @@ abstract public function getUser(IteratorFilter $filter): UserModel|null; * @return UserModel|null * @throws InvalidArgumentException */ + #[\Override] public function getByEmail(string $email): UserModel|null { $filter = new IteratorFilter(); @@ -134,6 +141,7 @@ public function getByEmail(string $email): UserModel|null * @return UserModel|null * @throws InvalidArgumentException */ + #[\Override] public function getByUsername(string $username): UserModel|null { $filter = new IteratorFilter(); @@ -148,6 +156,7 @@ public function getByUsername(string $username): UserModel|null * @param string $login * @return UserModel|null */ + #[\Override] public function getByLoginField(string $login): UserModel|null { $filter = new IteratorFilter(); @@ -164,6 +173,7 @@ public function getByLoginField(string $login): UserModel|null * @return UserModel|null * @throws InvalidArgumentException */ + #[\Override] public function getById(string|HexUuidLiteral|int $userid): UserModel|null { $filter = new IteratorFilter(); @@ -177,6 +187,7 @@ public function getById(string|HexUuidLiteral|int $userid): UserModel|null * @param string $login * @return bool * */ + #[\Override] abstract public function removeByLoginField(string $login): bool; /** @@ -188,6 +199,7 @@ abstract public function removeByLoginField(string $login): bool; * @return UserModel|null * @throws InvalidArgumentException */ + #[\Override] public function isValidUser(string $userName, string $password): UserModel|null { $filter = new IteratorFilter(); @@ -212,6 +224,7 @@ public function isValidUser(string $userName, string $password): UserModel|null * @throws UserNotFoundException * @throws InvalidArgumentException */ + #[\Override] public function hasProperty(string|int|HexUuidLiteral|null $userId, string $propertyName, string $value = null): bool { //anydataset.Row @@ -248,6 +261,7 @@ public function hasProperty(string|int|HexUuidLiteral|null $userId, string $prop * @throws UserNotFoundException * @throws InvalidArgumentException */ + #[\Override] public function getProperty(string|HexUuidLiteral|int $userId, string $propertyName): array|string|UserPropertiesModel|null { $user = $this->getById($userId); @@ -274,6 +288,7 @@ abstract public function getUsersByPropertySet(array $propertiesArray): array; * @param string $propertyName * @param string|null $value */ + #[\Override] abstract public function addProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value): bool; /** @@ -285,6 +300,7 @@ abstract public function addProperty(string|HexUuidLiteral|int $userId, string $ * @param string|null $value Property value with a site * @return bool * */ + #[\Override] abstract public function removeProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value = null): bool; /** @@ -295,6 +311,7 @@ abstract public function removeProperty(string|HexUuidLiteral|int $userId, strin * @param string|null $value Property value with a site * @return void * */ + #[\Override] abstract public function removeAllProperties(string $propertyName, string|null $value = null): void; /** @@ -303,6 +320,7 @@ abstract public function removeAllProperties(string $propertyName, string|null $ * @throws UserNotFoundException * @throws InvalidArgumentException */ + #[\Override] public function isAdmin(string|HexUuidLiteral|int $userId): bool { $user = $this->getById($userId); @@ -329,6 +347,7 @@ public function isAdmin(string|HexUuidLiteral|int $userId): bool * @throws UserNotFoundException * @throws InvalidArgumentException */ + #[\Override] public function createAuthToken( string $login, string $password, @@ -373,6 +392,7 @@ public function createAuthToken( * @throws NotAuthenticatedException * @throws UserNotFoundException */ + #[\Override] public function isValidToken(string $login, JwtWrapper $jwtWrapper, string $token): array|null { $user = $this->getByLoginField($login); @@ -398,5 +418,6 @@ public function isValidToken(string $login, JwtWrapper $jwtWrapper, string $toke /** * @param string|int|HexUuidLiteral $userid */ + #[\Override] abstract public function removeUserById(string|HexUuidLiteral|int $userid): bool; } diff --git a/src/UsersDBDataset.php b/src/UsersDBDataset.php index 8c29126..e1a2847 100644 --- a/src/UsersDBDataset.php +++ b/src/UsersDBDataset.php @@ -118,6 +118,7 @@ public function __construct( * @throws OrmInvalidFieldsException * @throws Exception */ + #[\Override] public function save(UserModel $model): UserModel { $newUser = false; @@ -176,6 +177,7 @@ public function getIterator(IteratorFilter $filter = null): array * @param IteratorFilter $filter Filter to find user * @return UserModel|null */ + #[\Override] public function getUser(IteratorFilter $filter): UserModel|null { $result = $this->getIterator($filter); @@ -197,6 +199,7 @@ public function getUser(IteratorFilter $filter): UserModel|null * @return bool * @throws Exception */ + #[\Override] public function removeByLoginField(string $login): bool { $user = $this->getByLoginField($login); @@ -215,6 +218,7 @@ public function removeByLoginField(string $login): bool * @return bool * @throws Exception */ + #[\Override] public function removeUserById(string|HexUuidLiteral|int $userid): bool { $updateTableProperties = DeleteQuery::getInstance() @@ -241,6 +245,7 @@ public function removeUserById(string|HexUuidLiteral|int $userid): bool * @throws InvalidArgumentException * @throws ExceptionInvalidArgumentException */ + #[\Override] public function getUsersByProperty(string $propertyName, string $value): array { return $this->getUsersByPropertySet([$propertyName => $value]); @@ -254,6 +259,7 @@ public function getUsersByProperty(string $propertyName, string $value): array * @throws InvalidArgumentException * @throws ExceptionInvalidArgumentException */ + #[\Override] public function getUsersByPropertySet(array $propertiesArray): array { $query = Query::getInstance() @@ -281,6 +287,7 @@ public function getUsersByPropertySet(array $propertiesArray): array * @throws OrmInvalidFieldsException * @throws Exception */ + #[\Override] public function addProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value): bool { //anydataset.Row @@ -306,6 +313,7 @@ public function addProperty(string|HexUuidLiteral|int $userId, string $propertyN * @throws ExceptionInvalidArgumentException * @throws OrmInvalidFieldsException */ + #[\Override] public function setProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value): bool { $query = Query::getInstance() @@ -339,6 +347,7 @@ public function setProperty(string|HexUuidLiteral|int $userId, string $propertyN * @throws InvalidArgumentException * @throws RepositoryReadOnlyException */ + #[\Override] public function removeProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value = null): bool { $user = $this->getById($userId); @@ -371,6 +380,7 @@ public function removeProperty(string|HexUuidLiteral|int $userId, string $proper * @throws ExceptionInvalidArgumentException * @throws RepositoryReadOnlyException */ + #[\Override] public function removeAllProperties(string $propertyName, string|null $value = null): void { $updateable = DeleteQuery::getInstance() @@ -384,6 +394,7 @@ public function removeAllProperties(string $propertyName, string|null $value = n $this->propertiesRepository->deleteByQuery($updateable); } + #[\Override] public function getProperty(string|HexUuidLiteral|int $userId, string $propertyName): array|string|UserPropertiesModel|null { $query = Query::getInstance() diff --git a/tests/PasswordDefinitionTest.php b/tests/PasswordDefinitionTest.php index 3454239..106f44d 100644 --- a/tests/PasswordDefinitionTest.php +++ b/tests/PasswordDefinitionTest.php @@ -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() + { + 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() + { + 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() + { + 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/SessionContextTest.php b/tests/SessionContextTest.php index 33b1067..f967ba3 100644 --- a/tests/SessionContextTest.php +++ b/tests/SessionContextTest.php @@ -14,11 +14,13 @@ 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; diff --git a/tests/UserModelTest.php b/tests/UserModelTest.php index a63b081..b0e1c81 100644 --- a/tests/UserModelTest.php +++ b/tests/UserModelTest.php @@ -15,11 +15,13 @@ class UserModelTest extends TestCase */ protected $object; + #[\Override] public function setUp(): void { $this->object = new UserModel(); } + #[\Override] public function tearDown(): void { $this->object = null; diff --git a/tests/UsersAnyDataset2ByEmailTest.php b/tests/UsersAnyDataset2ByEmailTest.php index 19ba913..f3039db 100644 --- a/tests/UsersAnyDataset2ByEmailTest.php +++ b/tests/UsersAnyDataset2ByEmailTest.php @@ -6,6 +6,7 @@ class UsersAnyDataset2EmailTest extends UsersAnyDatasetByUsernameTest { + #[\Override] public function setUp(): void { $this->__setUp(UserDefinition::LOGIN_IS_EMAIL); diff --git a/tests/UsersAnyDataset2ByUsernameTest.php b/tests/UsersAnyDataset2ByUsernameTest.php index 22d41e9..50eaa86 100644 --- a/tests/UsersAnyDataset2ByUsernameTest.php +++ b/tests/UsersAnyDataset2ByUsernameTest.php @@ -11,6 +11,7 @@ class UsersAnyDataset2ByUsernameTest extends UsersAnyDatasetByUsernameTest { + #[\Override] public function __setUp($loginField) { $this->prefix = "user"; @@ -50,6 +51,7 @@ public function __setUp($loginField) $this->object->addUser('User 3', 'user3', 'user3@gmail.com', 'pwd3'); } + #[\Override] public function setUp(): void { $this->__setUp(UserDefinition::LOGIN_IS_USERNAME); diff --git a/tests/UsersAnyDatasetByEmailTest.php b/tests/UsersAnyDatasetByEmailTest.php index 560ba99..b7ad3bf 100644 --- a/tests/UsersAnyDatasetByEmailTest.php +++ b/tests/UsersAnyDatasetByEmailTest.php @@ -6,6 +6,7 @@ class UsersAnyDatasetByEmailTest extends UsersAnyDatasetByUsernameTest { + #[\Override] public function setUp(): void { $this->__setUp(UserDefinition::LOGIN_IS_EMAIL); diff --git a/tests/UsersAnyDatasetByUsernameTest.php b/tests/UsersAnyDatasetByUsernameTest.php index c6b957c..d7cb2b3 100644 --- a/tests/UsersAnyDatasetByUsernameTest.php +++ b/tests/UsersAnyDatasetByUsernameTest.php @@ -61,6 +61,7 @@ public function __chooseValue($forUsername, $forEmail) return $searchForList[$this->userDefinition->loginField()]; } + #[\Override] public function setUp(): void { $this->__setUp(UserDefinition::LOGIN_IS_USERNAME); diff --git a/tests/UsersDBDataset2ByEmailTest.php b/tests/UsersDBDataset2ByEmailTest.php index 694f024..8a33475 100644 --- a/tests/UsersDBDataset2ByEmailTest.php +++ b/tests/UsersDBDataset2ByEmailTest.php @@ -6,6 +6,7 @@ class UsersDBDataset2ByEmailTest extends UsersDBDatasetByUsernameTest { + #[\Override] public function setUp(): void { $this->__setUp(UserDefinition::LOGIN_IS_EMAIL); diff --git a/tests/UsersDBDataset2ByUserNameTest.php b/tests/UsersDBDataset2ByUserNameTest.php index 382179c..a13cc78 100644 --- a/tests/UsersDBDataset2ByUserNameTest.php +++ b/tests/UsersDBDataset2ByUserNameTest.php @@ -17,6 +17,7 @@ class UsersDBDataset2ByUserNameTest extends UsersDBDatasetByUsernameTest * @throws \ReflectionException * @throws OrmModelInvalidException */ + #[\Override] public function __setUp($loginField) { $this->prefix = ""; @@ -68,6 +69,7 @@ public function __setUp($loginField) $this->object->addUser('User 3', 'user3', 'user3@gmail.com', 'pwd3'); } + #[\Override] public function setUp(): void { $this->__setUp(UserDefinition::LOGIN_IS_USERNAME); diff --git a/tests/UsersDBDatasetByEmailTest.php b/tests/UsersDBDatasetByEmailTest.php index edaa39b..e37efe5 100644 --- a/tests/UsersDBDatasetByEmailTest.php +++ b/tests/UsersDBDatasetByEmailTest.php @@ -6,6 +6,7 @@ class UsersDBDatasetByEmailTest extends UsersAnyDatasetByUsernameTest { + #[\Override] public function setUp(): void { $this->__setUp(UserDefinition::LOGIN_IS_EMAIL); diff --git a/tests/UsersDBDatasetByUsernameTest.php b/tests/UsersDBDatasetByUsernameTest.php index 52df602..d74b4a6 100644 --- a/tests/UsersDBDatasetByUsernameTest.php +++ b/tests/UsersDBDatasetByUsernameTest.php @@ -16,6 +16,7 @@ class UsersDBDatasetByUsernameTest extends UsersAnyDatasetByUsernameTest protected $db; + #[\Override] public function __setUp($loginField) { $this->prefix = ""; @@ -59,11 +60,13 @@ public function __setUp($loginField) $this->object->addUser('User 3', 'user3', 'user3@gmail.com', 'pwd3'); } + #[\Override] public function setUp(): void { $this->__setUp(UserDefinition::LOGIN_IS_USERNAME); } + #[\Override] public function tearDown(): void { $uri = new Uri(self::CONNECTION_STRING); @@ -73,6 +76,7 @@ public function tearDown(): void $this->propertyDefinition = null; } + #[\Override] public function testAddUser() { $this->object->addUser('John Doe', 'john', 'johndoe@gmail.com', 'mypassword'); @@ -96,6 +100,7 @@ public function testAddUser() $this->assertEquals('y', $user2->getAdmin()); } + #[\Override] public function testCreateAuthToken() { $login = $this->__chooseValue('user2', 'user2@gmail.com'); @@ -153,6 +158,7 @@ public function testWithUpdateValue() $this->assertEquals('2017-12-04 00:00:00', $user->getCreated()); } + #[\Override] public function testSaveAndSave() { $user = $this->object->getById("1"); diff --git a/tests/UsersDBDatasetDefinitionTest.php b/tests/UsersDBDatasetDefinitionTest.php index a568842..2edf0cd 100644 --- a/tests/UsersDBDatasetDefinitionTest.php +++ b/tests/UsersDBDatasetDefinitionTest.php @@ -48,6 +48,7 @@ class UsersDBDatasetDefinitionTest extends UsersDBDatasetByUsernameTest * @throws UserExistsException * @throws ReflectionException */ + #[\Override] public function __setUp($loginField) { $this->prefix = ""; @@ -113,6 +114,7 @@ public function __setUp($loginField) * @throws ReflectionException * @throws UserExistsException */ + #[\Override] public function setUp(): void { $this->__setUp(UserDefinition::LOGIN_IS_USERNAME); @@ -123,6 +125,7 @@ public function setUp(): void * @throws DatabaseException * @throws \ByJG\Serializer\Exception\InvalidArgumentException */ + #[\Override] public function testAddUser() { $this->object->save(new MyUserModel('John Doe', 'johndoe@gmail.com', 'john', 'mypassword', 'no', 'other john')); @@ -151,6 +154,7 @@ public function testAddUser() /** * @throws Exception */ + #[\Override] public function testWithUpdateValue() { // For Update Definitions From 6c160aa918a0c6e7200461227f3980a2d96aaf23 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Sat, 1 Nov 2025 15:30:20 -0400 Subject: [PATCH 02/40] Refactor `UserDefinition` to support `MapperFunctionInterface` for extensible field mapping; add new mappers (`ClosureMapper`, `PasswordSha1Mapper`, `UserIdGeneratorMapper`) and processors (`PassThroughEntityProcessor`, `ClosureEntityProcessor`); update dependencies and increase type safety across the codebase. --- .claude/settings.local.json | 9 ++ .idea/runConfigurations/psalm.xml | 8 + phpunit.xml.dist | 30 ++-- src/Definition/UserDefinition.php | 144 ++++++++++++------ .../ClosureEntityProcessor.php | 31 ++++ .../PassThroughEntityProcessor.php | 16 ++ src/Interfaces/UsersInterface.php | 2 +- src/MapperFunctions/ClosureMapper.php | 34 +++++ src/MapperFunctions/PasswordSha1Mapper.php | 28 ++++ src/MapperFunctions/UserIdGeneratorMapper.php | 28 ++++ src/UsersAnyDataset.php | 71 +++++---- src/UsersBase.php | 2 +- src/UsersDBDataset.php | 43 ++++-- tests/UsersAnyDataset2ByEmailTest.php | 2 +- tests/UsersDBDatasetByUsernameTest.php | 33 ++-- tests/UsersDBDatasetDefinitionTest.php | 41 ++--- 16 files changed, 381 insertions(+), 141 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 .idea/runConfigurations/psalm.xml create mode 100644 src/EntityProcessors/ClosureEntityProcessor.php create mode 100644 src/EntityProcessors/PassThroughEntityProcessor.php create mode 100644 src/MapperFunctions/ClosureMapper.php create mode 100644 src/MapperFunctions/PasswordSha1Mapper.php create mode 100644 src/MapperFunctions/UserIdGeneratorMapper.php diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..46507b9 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(find:*)" + ], + "deny": [], + "ask": [] + } +} 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/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/src/Definition/UserDefinition.php b/src/Definition/UserDefinition.php index 1019c98..cf1c0ed 100644 --- a/src/Definition/UserDefinition.php +++ b/src/Definition/UserDefinition.php @@ -2,8 +2,15 @@ namespace ByJG\Authenticate\Definition; +use ByJG\Authenticate\EntityProcessors\ClosureEntityProcessor; +use ByJG\Authenticate\EntityProcessors\PassThroughEntityProcessor; +use ByJG\Authenticate\MapperFunctions\ClosureMapper; +use ByJG\Authenticate\MapperFunctions\PasswordSha1Mapper; use ByJG\Authenticate\Model\UserModel; -use ByJG\MicroOrm\MapperClosure; +use ByJG\MicroOrm\Interface\EntityProcessorInterface; +use ByJG\MicroOrm\Interface\MapperFunctionInterface; +use ByJG\MicroOrm\MapperFunctions\ReadOnlyMapper; +use ByJG\MicroOrm\MapperFunctions\StandardMapper; use ByJG\Serializer\Serialize; use Closure; use InvalidArgumentException; @@ -14,7 +21,7 @@ class UserDefinition { protected string $__table = 'users'; - protected array $__closures = ["select" => [], "update" => [] ]; + protected array $__mappers = ["select" => [], "update" => [] ]; protected string $__loginField; protected string $__model; protected array $__properties = []; @@ -65,32 +72,15 @@ public function __construct( $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)); - }); + $this->defineMapperForUpdate(UserDefinition::FIELD_PASSWORD, PasswordSha1Mapper::class); 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; - }; + $this->beforeInsert = new PassThroughEntityProcessor(); + $this->beforeUpdate = new PassThroughEntityProcessor(); } /** @@ -140,58 +130,98 @@ private function checkProperty(string $property): void /** * @param string $event * @param string $property - * @param Closure $closure + * @param MapperFunctionInterface|string $mapper */ - private function updateClosureDef(string $event, string $property, Closure $closure): void + private function updateMapperDef(string $event, string $property, MapperFunctionInterface|string $mapper): void { $this->checkProperty($property); - $this->__closures[$event][$property] = $closure; + $this->__mappers[$event][$property] = $mapper; } - private function getClosureDef(string $event, string $property): Closure + private function getMapperDef(string $event, string $property): MapperFunctionInterface|string { $this->checkProperty($property); - if (!$this->existsClosure($event, $property)) { - return MapperClosure::standard(); + if (!$this->existsMapper($event, $property)) { + return StandardMapper::class; } - return $this->__closures[$event][$property]; + return $this->__mappers[$event][$property]; } - public function existsClosure(string $event, string $property): bool + public function existsMapper(string $event, string $property): bool { // Event not set - if (!isset($this->__closures[$event])) { + if (!isset($this->__mappers[$event])) { return false; } // Event is set but there is no property - if (!array_key_exists($property, $this->__closures[$event])) { + if (!array_key_exists($property, $this->__mappers[$event])) { return false; } return true; } + /** + * @deprecated Use existsMapper instead + */ + public function existsClosure(string $event, string $property): bool + { + return $this->existsMapper($event, $property); + } + public function markPropertyAsReadOnly(string $property): void { - $this->updateClosureDef(self::UPDATE, $property, MapperClosure::readOnly()); + $this->updateMapperDef(self::UPDATE, $property, ReadOnlyMapper::class); + } + + public function defineMapperForUpdate(string $property, MapperFunctionInterface|string $mapper): void + { + $this->updateMapperDef(self::UPDATE, $property, $mapper); } + public function defineMapperForSelect(string $property, MapperFunctionInterface|string $mapper): void + { + $this->updateMapperDef(self::SELECT, $property, $mapper); + } + + /** + * @deprecated Use defineMapperForUpdate instead + */ public function defineClosureForUpdate(string $property, Closure $closure): void { - $this->updateClosureDef(self::UPDATE, $property, $closure); + $this->updateMapperDef(self::UPDATE, $property, new ClosureMapper($closure)); } + /** + * @deprecated Use defineMapperForSelect instead + */ public function defineClosureForSelect(string $property, Closure $closure): void { - $this->updateClosureDef(self::SELECT, $property, $closure); + $this->updateMapperDef(self::SELECT, $property, new ClosureMapper($closure)); + } + + public function getMapperForUpdate(string $property): MapperFunctionInterface|string + { + return $this->getMapperDef(self::UPDATE, $property); } + /** + * @deprecated Use getMapperForUpdate instead. Returns a Closure for backward compatibility. + */ public function getClosureForUpdate(string $property): Closure { - return $this->getClosureDef(self::UPDATE, $property); + $mapper = $this->getMapperDef(self::UPDATE, $property); + + // Return a closure that wraps the mapper + return function($value, $instance = null) use ($mapper) { + if (is_string($mapper)) { + $mapper = new $mapper(); + } + return $mapper->processedValue($value, $instance, null); + }; } public function defineGenerateKeyClosure(Closure $closure): void @@ -204,13 +234,27 @@ public function getGenerateKeyClosure(): ?Closure return $this->__generateKey; } + public function getMapperForSelect(string $property): MapperFunctionInterface|string + { + return $this->getMapperDef(self::SELECT, $property); + } + /** + * @deprecated Use getMapperForSelect instead. Returns a Closure for backward compatibility. * @param $property * @return Closure */ public function getClosureForSelect($property): Closure { - return $this->getClosureDef(self::SELECT, $property); + $mapper = $this->getMapperDef(self::SELECT, $property); + + // Return a closure that wraps the mapper + return function($value, $instance = null) use ($mapper) { + if (is_string($mapper)) { + $mapper = new $mapper(); + } + return $mapper->processedValue($value, $instance, null); + }; } public function model(): string @@ -224,39 +268,45 @@ public function modelInstance(): UserModel return new $model(); } - protected Closure $beforeInsert; + protected EntityProcessorInterface $beforeInsert; /** - * @return Closure + * @return EntityProcessorInterface */ - public function getBeforeInsert(): Closure + public function getBeforeInsert(): EntityProcessorInterface { return $this->beforeInsert; } /** - * @param Closure $beforeInsert + * @param EntityProcessorInterface|Closure $beforeInsert */ - public function setBeforeInsert(Closure $beforeInsert): void + public function setBeforeInsert(EntityProcessorInterface|Closure $beforeInsert): void { + if ($beforeInsert instanceof Closure) { + $beforeInsert = new ClosureEntityProcessor($beforeInsert); + } $this->beforeInsert = $beforeInsert; } - protected Closure $beforeUpdate; + protected EntityProcessorInterface $beforeUpdate; /** - * @return Closure + * @return EntityProcessorInterface */ - public function getBeforeUpdate(): Closure + public function getBeforeUpdate(): EntityProcessorInterface { return $this->beforeUpdate; } /** - * @param mixed $beforeUpdate + * @param EntityProcessorInterface|Closure $beforeUpdate */ - public function setBeforeUpdate(Closure $beforeUpdate): void + public function setBeforeUpdate(EntityProcessorInterface|Closure $beforeUpdate): void { + if ($beforeUpdate instanceof Closure) { + $beforeUpdate = new ClosureEntityProcessor($beforeUpdate); + } $this->beforeUpdate = $beforeUpdate; } } diff --git a/src/EntityProcessors/ClosureEntityProcessor.php b/src/EntityProcessors/ClosureEntityProcessor.php new file mode 100644 index 0000000..efd4d17 --- /dev/null +++ b/src/EntityProcessors/ClosureEntityProcessor.php @@ -0,0 +1,31 @@ +closure = $closure; + } + + public function process(array $instance): array + { + $result = ($this->closure)($instance); + + // If closure returns an object, convert it to array + if (is_object($result)) { + return (array) $result; + } + + return $result; + } +} diff --git a/src/EntityProcessors/PassThroughEntityProcessor.php b/src/EntityProcessors/PassThroughEntityProcessor.php new file mode 100644 index 0000000..54569f3 --- /dev/null +++ b/src/EntityProcessors/PassThroughEntityProcessor.php @@ -0,0 +1,16 @@ +closure = $closure; + } + + public function processedValue(mixed $value, mixed $instance, mixed $helper = null): mixed + { + $reflection = new ReflectionFunction($this->closure); + $paramCount = $reflection->getNumberOfParameters(); + + // Call closure with appropriate number of parameters + return match($paramCount) { + 1 => ($this->closure)($value), + 2 => ($this->closure)($value, $instance), + default => ($this->closure)($value, $instance, $helper) + }; + } +} diff --git a/src/MapperFunctions/PasswordSha1Mapper.php b/src/MapperFunctions/PasswordSha1Mapper.php new file mode 100644 index 0000000..d995ff5 --- /dev/null +++ b/src/MapperFunctions/PasswordSha1Mapper.php @@ -0,0 +1,28 @@ +getUsername())); + } + + return $value; + } +} diff --git a/src/UsersAnyDataset.php b/src/UsersAnyDataset.php index 2cda813..cb7300e 100644 --- a/src/UsersAnyDataset.php +++ b/src/UsersAnyDataset.php @@ -7,11 +7,12 @@ use ByJG\AnyDataset\Core\Exception\DatabaseException; use ByJG\AnyDataset\Core\IteratorFilter; use ByJG\AnyDataset\Core\IteratorInterface; -use ByJG\AnyDataset\Core\Row; +use ByJG\AnyDataset\Core\RowInterface; use ByJG\Authenticate\Definition\UserDefinition; use ByJG\Authenticate\Definition\UserPropertiesDefinition; use ByJG\Authenticate\Exception\UserExistsException; use ByJG\Authenticate\Exception\UserNotFoundException; +use ByJG\Authenticate\MapperFunctions\UserIdGeneratorMapper; use ByJG\Authenticate\Model\UserModel; use ByJG\Authenticate\Model\UserPropertiesModel; use ByJG\MicroOrm\Literal\HexUuidLiteral; @@ -38,19 +39,14 @@ class UsersAnyDataset extends UsersBase */ public function __construct( AnyDataset $anyDataset, - UserDefinition $userTable = null, - UserPropertiesDefinition $propertiesTable = null + UserDefinition|null $userTable = null, + UserPropertiesDefinition|null $propertiesTable = null ) { $this->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; - }); + if (!$userTable->existsMapper('update', UserDefinition::FIELD_USERID)) { + $userTable->defineMapperForUpdate(UserDefinition::FIELD_USERID, UserIdGeneratorMapper::class); } $this->propertiesTable = $propertiesTable; } @@ -86,9 +82,23 @@ public function save(UserModel $model): UserModel } } - $properties = $model->getProperties(); - foreach ($properties as $value) { - $this->anyDataSet->addField($value->getName(), $value->getValue()); + // Group properties by name to handle multiple values + $propertiesByName = []; + foreach ($model->getProperties() as $property) { + $name = $property->getName(); + if (!isset($propertiesByName[$name])) { + $propertiesByName[$name] = []; + } + $propertiesByName[$name][] = $property->getValue(); + } + + // Add properties, using array if multiple values exist + foreach ($propertiesByName as $name => $values) { + if (count($values) === 1) { + $this->anyDataSet->addField($name, $values[0]); + } else { + $this->anyDataSet->addField($name, $values); + } } $this->anyDataSet->save(); @@ -108,11 +118,11 @@ public function save(UserModel $model): UserModel public function getUser(IteratorFilter $filter): UserModel|null { $iterator = $this->anyDataSet->getIterator($filter); - if (!$iterator->hasNext()) { + if (!$iterator->valid()) { return null; } - return $this->createUserModel($iterator->moveNext()); + return $this->createUserModel($iterator->current()); } /** @@ -133,8 +143,8 @@ public function removeByLoginField(string $login): bool $iteratorFilter->and($this->getUserDefinition()->loginField(), Relation::EQUAL, $login); $iterator = $this->anyDataSet->getIterator($iteratorFilter); - if ($iterator->hasNext()) { - $oldRow = $iterator->moveNext(); + if ($iterator->valid()) { + $oldRow = $iterator->current(); $this->anyDataSet->removeRow($oldRow); $this->anyDataSet->save(); return true; @@ -149,7 +159,7 @@ public function removeByLoginField(string $login): bool * @param IteratorFilter|null $filter * @return IteratorInterface */ - public function getIterator(IteratorFilter $filter = null): IteratorInterface + public function getIterator(IteratorFilter|null $filter = null): IteratorInterface { return $this->anyDataSet->getIterator($filter); } @@ -270,19 +280,17 @@ public function removeProperty(string|HexUuidLiteral|int $userId, string $proper public function removeAllProperties(string $propertyName, string|null $value = null): void { $iterator = $this->getIterator(null); - while ($iterator->hasNext()) { + foreach ($iterator as $user) { //anydataset.Row - $user = $iterator->moveNext(); $this->removeProperty($user->get($this->getUserDefinition()->getUserid()), $propertyName, $value); } } /** - * @param Row $row + * @param RowInterface $row * @return UserModel - * @throws InvalidArgumentException */ - private function createUserModel(Row $row): UserModel + private function createUserModel(RowInterface $row): UserModel { $allProp = $row->toArray(); $userModel = new UserModel(); @@ -296,8 +304,17 @@ private function createUserModel(Row $row): UserModel } foreach (array_keys($allProp) as $property) { - foreach ($row->getAsArray($property) as $eachValue) { - $userModel->addProperty(new UserPropertiesModel($property, $eachValue)); + $values = $row->get($property); + + // Handle both single values and arrays + if (!is_array($values)) { + if ($values !== null) { + $userModel->addProperty(new UserPropertiesModel($property, $values)); + } + } else { + foreach ($values as $eachValue) { + $userModel->addProperty(new UserPropertiesModel($property, $eachValue)); + } } } @@ -316,8 +333,8 @@ public function removeUserById(string|HexUuidLiteral|int $userid): bool $iteratorFilter->and($this->getUserDefinition()->getUserid(), Relation::EQUAL, $userid); $iterator = $this->anyDataSet->getIterator($iteratorFilter); - if ($iterator->hasNext()) { - $oldRow = $iterator->moveNext(); + if ($iterator->valid()) { + $oldRow = $iterator->current(); $this->anyDataSet->removeRow($oldRow); return true; } diff --git a/src/UsersBase.php b/src/UsersBase.php index c82b850..e286db0 100644 --- a/src/UsersBase.php +++ b/src/UsersBase.php @@ -225,7 +225,7 @@ public function isValidUser(string $userName, string $password): UserModel|null * @throws InvalidArgumentException */ #[\Override] - public function hasProperty(string|int|HexUuidLiteral|null $userId, string $propertyName, string $value = null): bool + public function hasProperty(string|int|HexUuidLiteral|null $userId, string $propertyName, string|null $value = null): bool { //anydataset.Row $user = $this->getById($userId); diff --git a/src/UsersDBDataset.php b/src/UsersDBDataset.php index e1a2847..d502fe2 100644 --- a/src/UsersDBDataset.php +++ b/src/UsersDBDataset.php @@ -3,6 +3,7 @@ namespace ByJG\Authenticate; use ByJG\AnyDataset\Core\IteratorFilter; +use ByJG\AnyDataset\Db\DatabaseExecutor; use ByJG\AnyDataset\Db\DbDriverInterface; use ByJG\AnyDataset\Db\IteratorFilterSqlFormatter; use ByJG\Authenticate\Definition\UserDefinition; @@ -41,14 +42,14 @@ class UsersDBDataset extends UsersBase protected Repository $propertiesRepository; /** - * @var DbDriverInterface + * @var DatabaseExecutor */ - protected DbDriverInterface $provider; + protected DatabaseExecutor $executor; /** * UsersDBDataset constructor * - * @param DbDriverInterface $dbDriver + * @param DbDriverInterface|DatabaseExecutor $dbDriver * @param UserDefinition|null $userTable * @param UserPropertiesDefinition|null $propertiesTable * @@ -56,10 +57,16 @@ class UsersDBDataset extends UsersBase * @throws ReflectionException */ public function __construct( - DbDriverInterface $dbDriver, - UserDefinition $userTable = null, - UserPropertiesDefinition $propertiesTable = null + DbDriverInterface|DatabaseExecutor $dbDriver, + UserDefinition|null $userTable = null, + UserPropertiesDefinition|null $propertiesTable = null ) { + // Convert DbDriverInterface to DatabaseExecutor if needed + if ($dbDriver instanceof DbDriverInterface && !($dbDriver instanceof DatabaseExecutor)) { + $dbDriver = new DatabaseExecutor($dbDriver); + } + $this->executor = $dbDriver; + if (empty($userTable)) { $userTable = new UserDefinition(); } @@ -83,11 +90,11 @@ public function __construct( foreach ($propertyDefinition as $property => $map) { $userMapper->addFieldMapping(FieldMapping::create($property) ->withFieldName($map) - ->withUpdateFunction($userTable->getClosureForUpdate($property)) - ->withSelectFunction($userTable->getClosureForSelect($property)) + ->withUpdateFunction($userTable->getMapperForUpdate($property)) + ->withSelectFunction($userTable->getMapperForSelect($property)) ); } - $this->userRepository = new Repository($dbDriver, $userMapper); + $this->userRepository = new Repository($this->executor, $userMapper); $propertiesMapper = new Mapper( UserPropertiesModel::class, @@ -99,10 +106,10 @@ public function __construct( $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)) + ->withUpdateFunction($userTable->getMapperForUpdate(UserDefinition::FIELD_USERID)) + ->withSelectFunction($userTable->getMapperForSelect(UserDefinition::FIELD_USERID)) ); - $this->propertiesRepository = new Repository($dbDriver, $propertiesMapper); + $this->propertiesRepository = new Repository($this->executor, $propertiesMapper); $this->userTable = $userTable; $this->propertiesTable = $propertiesTable; @@ -114,6 +121,7 @@ public function __construct( * @param UserModel $model * @return UserModel * @throws UserExistsException + * @throws UserNotFoundException * @throws OrmBeforeInvalidException * @throws OrmInvalidFieldsException * @throws Exception @@ -137,11 +145,11 @@ public function save(UserModel $model): UserModel } if ($newUser) { - $model = $this->getByEmail($model->getEmail()); + $model = $this->getById($model->getUserid()); } if ($model === null) { - throw new UserExistsException("User not found"); + throw new UserNotFoundException("User not found"); } return $model; @@ -153,7 +161,7 @@ public function save(UserModel $model): UserModel * @param IteratorFilter|null $filter Filter to find user * @return UserModel[] */ - public function getIterator(IteratorFilter $filter = null): array + public function getIterator(IteratorFilter|null $filter = null): array { if (is_null($filter)) { $filter = new IteratorFilter(); @@ -426,7 +434,10 @@ public function getProperty(string|HexUuidLiteral|int $userId, string $propertyN */ protected function setPropertiesInUser(UserModel $userRow): void { - $value = $this->propertiesRepository->getMapper()->getFieldMap(UserDefinition::FIELD_USERID)->getUpdateFunctionValue($userRow->getUserid(), $userRow, $this->propertiesRepository->getDbDriverWrite()->getDbHelper()); + $value = $this->propertiesRepository + ->getMapper() + ->getFieldMap(UserDefinition::FIELD_USERID) + ->getUpdateFunctionValue($userRow->getUserid(), $userRow, $this->propertiesRepository->getExecutorWrite()->getHelper()); $query = Query::getInstance() ->table($this->getUserPropertiesDefinition()->table()) ->where("{$this->getUserPropertiesDefinition()->getUserid()} = :id", ['id' => $value]); diff --git a/tests/UsersAnyDataset2ByEmailTest.php b/tests/UsersAnyDataset2ByEmailTest.php index f3039db..2091e71 100644 --- a/tests/UsersAnyDataset2ByEmailTest.php +++ b/tests/UsersAnyDataset2ByEmailTest.php @@ -4,7 +4,7 @@ use ByJG\Authenticate\Definition\UserDefinition; -class UsersAnyDataset2EmailTest extends UsersAnyDatasetByUsernameTest +class UsersAnyDataset2ByEmailTest extends UsersAnyDatasetByUsernameTest { #[\Override] public function setUp(): void diff --git a/tests/UsersDBDatasetByUsernameTest.php b/tests/UsersDBDatasetByUsernameTest.php index d74b4a6..5b575a1 100644 --- a/tests/UsersDBDatasetByUsernameTest.php +++ b/tests/UsersDBDatasetByUsernameTest.php @@ -5,6 +5,7 @@ use ByJG\AnyDataset\Db\Factory; use ByJG\Authenticate\Definition\UserDefinition; use ByJG\Authenticate\Definition\UserPropertiesDefinition; +use ByJG\Authenticate\MapperFunctions\ClosureMapper; use ByJG\Authenticate\Model\UserModel; use ByJG\Authenticate\UsersDBDataset; use ByJG\Util\Uri; @@ -110,33 +111,33 @@ public function testCreateAuthToken() public function testWithUpdateValue() { // For Update Definitions - $this->userDefinition->defineClosureForUpdate(UserDefinition::FIELD_NAME, function ($value, $instance) { + $this->userDefinition->defineMapperForUpdate(UserDefinition::FIELD_NAME, new ClosureMapper(function ($value, $instance) { return '[' . $value . ']'; - }); - $this->userDefinition->defineClosureForUpdate(UserDefinition::FIELD_USERNAME, function ($value, $instance) { + })); + $this->userDefinition->defineMapperForUpdate(UserDefinition::FIELD_USERNAME, new ClosureMapper(function ($value, $instance) { return ']' . $value . '['; - }); - $this->userDefinition->defineClosureForUpdate(UserDefinition::FIELD_EMAIL, function ($value, $instance) { + })); + $this->userDefinition->defineMapperForUpdate(UserDefinition::FIELD_EMAIL, new ClosureMapper(function ($value, $instance) { return '-' . $value . '-'; - }); - $this->userDefinition->defineClosureForUpdate(UserDefinition::FIELD_PASSWORD, function ($value, $instance) { + })); + $this->userDefinition->defineMapperForUpdate(UserDefinition::FIELD_PASSWORD, new ClosureMapper(function ($value, $instance) { return "@" . $value . "@"; - }); + })); $this->userDefinition->markPropertyAsReadOnly(UserDefinition::FIELD_CREATED); // For Select Definitions - $this->userDefinition->defineClosureForSelect(UserDefinition::FIELD_NAME, function ($value, $instance) { + $this->userDefinition->defineMapperForSelect(UserDefinition::FIELD_NAME, new ClosureMapper(function ($value, $instance) { return '(' . $value . ')'; - }); - $this->userDefinition->defineClosureForSelect(UserDefinition::FIELD_USERNAME, function ($value, $instance) { + })); + $this->userDefinition->defineMapperForSelect(UserDefinition::FIELD_USERNAME, new ClosureMapper(function ($value, $instance) { return ')' . $value . '('; - }); - $this->userDefinition->defineClosureForSelect(UserDefinition::FIELD_EMAIL, function ($value, $instance) { + })); + $this->userDefinition->defineMapperForSelect(UserDefinition::FIELD_EMAIL, new ClosureMapper(function ($value, $instance) { return '#' . $value . '#'; - }); - $this->userDefinition->defineClosureForSelect(UserDefinition::FIELD_PASSWORD, function ($value, $instance) { + })); + $this->userDefinition->defineMapperForSelect(UserDefinition::FIELD_PASSWORD, new ClosureMapper(function ($value, $instance) { return '%'. $value . '%'; - }); + })); // Test it! $newObject = new UsersDBDataset( diff --git a/tests/UsersDBDatasetDefinitionTest.php b/tests/UsersDBDatasetDefinitionTest.php index 2edf0cd..82c05d6 100644 --- a/tests/UsersDBDatasetDefinitionTest.php +++ b/tests/UsersDBDatasetDefinitionTest.php @@ -7,6 +7,7 @@ use ByJG\Authenticate\Definition\UserDefinition; use ByJG\Authenticate\Definition\UserPropertiesDefinition; use ByJG\Authenticate\Exception\UserExistsException; +use ByJG\Authenticate\MapperFunctions\ClosureMapper; use ByJG\Authenticate\Model\UserModel; use ByJG\Authenticate\UsersDBDataset; use ByJG\MicroOrm\Exception\OrmBeforeInvalidException; @@ -158,39 +159,39 @@ public function testAddUser() public function testWithUpdateValue() { // For Update Definitions - $this->userDefinition->defineClosureForUpdate(UserDefinition::FIELD_NAME, function ($value, $instance) { + $this->userDefinition->defineMapperForUpdate(UserDefinition::FIELD_NAME, new ClosureMapper(function ($value, $instance) { return '[' . $value . ']'; - }); - $this->userDefinition->defineClosureForUpdate(UserDefinition::FIELD_USERNAME, function ($value, $instance) { + })); + $this->userDefinition->defineMapperForUpdate(UserDefinition::FIELD_USERNAME, new ClosureMapper(function ($value, $instance) { return ']' . $value . '['; - }); - $this->userDefinition->defineClosureForUpdate(UserDefinition::FIELD_EMAIL, function ($value, $instance) { + })); + $this->userDefinition->defineMapperForUpdate(UserDefinition::FIELD_EMAIL, new ClosureMapper(function ($value, $instance) { return '-' . $value . '-'; - }); - $this->userDefinition->defineClosureForUpdate(UserDefinition::FIELD_PASSWORD, function ($value, $instance) { + })); + $this->userDefinition->defineMapperForUpdate(UserDefinition::FIELD_PASSWORD, new ClosureMapper(function ($value, $instance) { return "@" . $value . "@"; - }); + })); $this->userDefinition->markPropertyAsReadOnly(UserDefinition::FIELD_CREATED); - $this->userDefinition->defineClosureForUpdate('otherfield', function ($value, $instance) { + $this->userDefinition->defineMapperForUpdate('otherfield', new ClosureMapper(function ($value, $instance) { return "*" . $value . "*"; - }); + })); // For Select Definitions - $this->userDefinition->defineClosureForSelect(UserDefinition::FIELD_NAME, function ($value, $instance) { + $this->userDefinition->defineMapperForSelect(UserDefinition::FIELD_NAME, new ClosureMapper(function ($value, $instance) { return '(' . $value . ')'; - }); - $this->userDefinition->defineClosureForSelect(UserDefinition::FIELD_USERNAME, function ($value, $instance) { + })); + $this->userDefinition->defineMapperForSelect(UserDefinition::FIELD_USERNAME, new ClosureMapper(function ($value, $instance) { return ')' . $value . '('; - }); - $this->userDefinition->defineClosureForSelect(UserDefinition::FIELD_EMAIL, function ($value, $instance) { + })); + $this->userDefinition->defineMapperForSelect(UserDefinition::FIELD_EMAIL, new ClosureMapper(function ($value, $instance) { return '#' . $value . '#'; - }); - $this->userDefinition->defineClosureForSelect(UserDefinition::FIELD_PASSWORD, function ($value, $instance) { + })); + $this->userDefinition->defineMapperForSelect(UserDefinition::FIELD_PASSWORD, new ClosureMapper(function ($value, $instance) { return '%' . $value . '%'; - }); - $this->userDefinition->defineClosureForSelect('otherfield', function ($value, $instance) { + })); + $this->userDefinition->defineMapperForSelect('otherfield', new ClosureMapper(function ($value, $instance) { return ']' . $value . '['; - }); + })); // Test it! $newObject = new UsersDBDataset( From 6eaf52708d3e6d669dc3b75dc3ff984335209f08 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Sat, 1 Nov 2025 16:00:53 -0400 Subject: [PATCH 03/40] Improve exception handling, replace deprecated methods with new implementations, enhance type hinting, support custom ID generation via `UniqueIdGeneratorInterface`, update test cases, and adjust dependencies for compatibility. --- composer.json | 2 +- src/Definition/PasswordDefinition.php | 4 + src/Definition/UserDefinition.php | 27 ++++-- src/Exception/NotAuthenticatedException.php | 4 +- src/Exception/NotImplementedException.php | 4 +- src/Exception/UserExistsException.php | 4 +- src/Exception/UserNotFoundException.php | 4 +- src/Interfaces/UsersInterface.php | 4 +- src/MapperFunctions/ClosureMapper.php | 4 + src/Model/UserModel.php | 2 +- src/SessionContext.php | 13 +-- src/UsersAnyDataset.php | 38 ++++---- src/UsersBase.php | 50 ++++++----- src/UsersDBDataset.php | 88 ++++++++++++++---- tests/UsersDBDatasetDefinitionTest.php | 99 +++++++++++++++++++++ 15 files changed, 271 insertions(+), 76 deletions(-) diff --git a/composer.json b/composer.json index ced7781..035f0c8 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "byjg/jwt-wrapper": "^6.0" }, "require-dev": { - "phpunit/phpunit": "^9.6", + "phpunit/phpunit": "^9.6|^11", "vimeo/psalm": "^5.9|^6.13" }, "scripts": { diff --git a/src/Definition/PasswordDefinition.php b/src/Definition/PasswordDefinition.php index ad27ece..12d0ff3 100644 --- a/src/Definition/PasswordDefinition.php +++ b/src/Definition/PasswordDefinition.php @@ -3,6 +3,7 @@ namespace ByJG\Authenticate\Definition; use InvalidArgumentException; +use Random\RandomException; class PasswordDefinition { @@ -113,6 +114,9 @@ public function matchPassword(string $password): int return $result; } + /** + * @throws RandomException + */ public function generatePassword(int $extendSize = 0): string { $charsList = [ diff --git a/src/Definition/UserDefinition.php b/src/Definition/UserDefinition.php index cf1c0ed..1cd3939 100644 --- a/src/Definition/UserDefinition.php +++ b/src/Definition/UserDefinition.php @@ -9,6 +9,7 @@ use ByJG\Authenticate\Model\UserModel; use ByJG\MicroOrm\Interface\EntityProcessorInterface; use ByJG\MicroOrm\Interface\MapperFunctionInterface; +use ByJG\MicroOrm\Interface\UniqueIdGeneratorInterface; use ByJG\MicroOrm\MapperFunctions\ReadOnlyMapper; use ByJG\MicroOrm\MapperFunctions\StandardMapper; use ByJG\Serializer\Serialize; @@ -25,7 +26,7 @@ class UserDefinition protected string $__loginField; protected string $__model; protected array $__properties = []; - protected Closure|null $__generateKey = null; + protected UniqueIdGeneratorInterface|string|null $__generateKey = null; const FIELD_USERID = 'userid'; const FIELD_NAME = 'name'; @@ -220,16 +221,32 @@ public function getClosureForUpdate(string $property): Closure if (is_string($mapper)) { $mapper = new $mapper(); } - return $mapper->processedValue($value, $instance, null); + return $mapper->processedValue($value, $instance); }; } + /** + * @deprecated Use defineGenerateKey instead + */ public function defineGenerateKeyClosure(Closure $closure): void { - $this->__generateKey = $closure; + throw new InvalidArgumentException('defineGenerateKeyClosure is deprecated. Use defineGenerateKey with UniqueIdGeneratorInterface instead.'); + } + + public function defineGenerateKey(UniqueIdGeneratorInterface|string $generator): void + { + $this->__generateKey = $generator; } - public function getGenerateKeyClosure(): ?Closure + public function getGenerateKey(): UniqueIdGeneratorInterface|string|null + { + return $this->__generateKey; + } + + /** + * @deprecated Use getGenerateKey instead + */ + public function getGenerateKeyClosure(): UniqueIdGeneratorInterface|string|null { return $this->__generateKey; } @@ -253,7 +270,7 @@ public function getClosureForSelect($property): Closure if (is_string($mapper)) { $mapper = new $mapper(); } - return $mapper->processedValue($value, $instance, null); + return $mapper->processedValue($value, $instance); }; } diff --git a/src/Exception/NotAuthenticatedException.php b/src/Exception/NotAuthenticatedException.php index 9685ec7..42f0982 100644 --- a/src/Exception/NotAuthenticatedException.php +++ b/src/Exception/NotAuthenticatedException.php @@ -2,7 +2,9 @@ namespace ByJG\Authenticate\Exception; -class NotAuthenticatedException extends \Exception +use Exception; + +class NotAuthenticatedException extends Exception { //put your code here } diff --git a/src/Exception/NotImplementedException.php b/src/Exception/NotImplementedException.php index 8756d4c..818ca3a 100644 --- a/src/Exception/NotImplementedException.php +++ b/src/Exception/NotImplementedException.php @@ -2,7 +2,9 @@ namespace ByJG\Authenticate\Exception; -class NotImplementedException extends \Exception +use Exception; + +class NotImplementedException extends Exception { //put your code here } diff --git a/src/Exception/UserExistsException.php b/src/Exception/UserExistsException.php index 20ab26d..5213ac0 100644 --- a/src/Exception/UserExistsException.php +++ b/src/Exception/UserExistsException.php @@ -2,7 +2,9 @@ namespace ByJG\Authenticate\Exception; -class UserExistsException extends \Exception +use Exception; + +class UserExistsException extends Exception { //put your code here } diff --git a/src/Exception/UserNotFoundException.php b/src/Exception/UserNotFoundException.php index 88bd3a9..8c9a657 100644 --- a/src/Exception/UserNotFoundException.php +++ b/src/Exception/UserNotFoundException.php @@ -2,7 +2,9 @@ namespace ByJG\Authenticate\Exception; -class UserNotFoundException extends \Exception +use Exception; + +class UserNotFoundException extends Exception { //put your code here } diff --git a/src/Interfaces/UsersInterface.php b/src/Interfaces/UsersInterface.php index 38003bc..82f12cc 100644 --- a/src/Interfaces/UsersInterface.php +++ b/src/Interfaces/UsersInterface.php @@ -113,7 +113,7 @@ public function hasProperty(string|int|HexUuidLiteral|null $userId, string $prop * @param string $propertyName * @return UserPropertiesModel|array|null|string String vector with all sites */ - public function getProperty(string|HexUuidLiteral|int $userId, string $propertyName): array|string|\ByJG\Authenticate\Model\UserPropertiesModel|null; + public function getProperty(string|HexUuidLiteral|int $userId, string $propertyName): array|string|UserPropertiesModel|null; /** * @@ -156,7 +156,7 @@ public function removeAllProperties(string $propertyName, string|null $value = n * @param int $expires * @param array $updateUserInfo * @param array $updateTokenInfo - * @return string|null Return the TOKEN or null, if can't create it. + * @return string|null Return the TOKEN or null, if we can't create it. */ public function createAuthToken( string $login, diff --git a/src/MapperFunctions/ClosureMapper.php b/src/MapperFunctions/ClosureMapper.php index 3fd4f17..fb63542 100644 --- a/src/MapperFunctions/ClosureMapper.php +++ b/src/MapperFunctions/ClosureMapper.php @@ -4,6 +4,7 @@ use ByJG\MicroOrm\Interface\MapperFunctionInterface; use Closure; +use ReflectionException; use ReflectionFunction; /** @@ -19,6 +20,9 @@ public function __construct(Closure $closure) $this->closure = $closure; } + /** + * @throws ReflectionException + */ public function processedValue(mixed $value, mixed $instance, mixed $helper = null): mixed { $reflection = new ReflectionFunction($this->closure); diff --git a/src/Model/UserModel.php b/src/Model/UserModel.php index 3dd4ee3..82cf36d 100644 --- a/src/Model/UserModel.php +++ b/src/Model/UserModel.php @@ -120,7 +120,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]"); } } diff --git a/src/SessionContext.php b/src/SessionContext.php index 5cbf846..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,7 +40,7 @@ public function __construct(CachePool $cachePool, string $key = 'default') * @return bool Return true if authenticated; false otherwise. * @throws InvalidArgumentException */ - #[\Override] + #[Override] public function isAuthenticated(): bool { $item = $this->session->getItem("user.$this->key"); @@ -53,7 +54,7 @@ public function isAuthenticated(): bool * @return string|int The authenticated username if exists. * @throws InvalidArgumentException */ - #[\Override] + #[Override] public function userInfo(): string|int { $item = $this->session->getItem("user.$this->key"); @@ -65,7 +66,7 @@ public function userInfo(): string|int * @param array $data * @throws InvalidArgumentException */ - #[\Override] + #[Override] public function registerLogin(string|int $userId, array $data = []): void { $item = $this->session->getItem("user.$this->key"); @@ -87,7 +88,7 @@ public function registerLogin(string|int $userId, array $data = []): void * @throws InvalidArgumentException * @throws InvalidArgumentException */ - #[\Override] + #[Override] public function setSessionData(string $name, mixed $value): void { if (!$this->isAuthenticated()) { @@ -114,7 +115,7 @@ public function setSessionData(string $name, mixed $value): void * @throws InvalidArgumentException * @throws InvalidArgumentException */ - #[\Override] + #[Override] public function getSessionData(string $name): mixed { if (!$this->isAuthenticated()) { @@ -142,7 +143,7 @@ public function getSessionData(string $name): mixed * @throws InvalidArgumentException * @throws InvalidArgumentException */ - #[\Override] + #[Override] public function registerLogout(): void { $this->session->deleteItem("user.$this->key"); diff --git a/src/UsersAnyDataset.php b/src/UsersAnyDataset.php index cb7300e..e184362 100644 --- a/src/UsersAnyDataset.php +++ b/src/UsersAnyDataset.php @@ -18,6 +18,7 @@ use ByJG\MicroOrm\Literal\HexUuidLiteral; use ByJG\Serializer\Exception\InvalidArgumentException; use ByJG\XmlUtil\Exception\FileException; +use Override; class UsersAnyDataset extends UsersBase { @@ -61,7 +62,7 @@ public function __construct( * @throws UserExistsException * @throws FileException */ - #[\Override] + #[Override] public function save(UserModel $model): UserModel { $new = true; @@ -75,8 +76,11 @@ public function save(UserModel $model): UserModel $propertyDefinition = $this->getUserDefinition()->toArray(); foreach ($propertyDefinition as $property => $map) { - $closure = $this->getUserDefinition()->getClosureForUpdate($property); - $value = $closure($model->{"get$property"}(), $model); + $mapper = $this->getUserDefinition()->getMapperForUpdate($property); + if (is_string($mapper)) { + $mapper = new $mapper(); + } + $value = $mapper->processedValue($model->{"get$property"}(), $model); if ($value !== false) { $this->anyDataSet->addField($map, $value); } @@ -112,9 +116,8 @@ public function save(UserModel $model): UserModel * * @param IteratorFilter $filter Filter to find user * @return UserModel|null - * @throws InvalidArgumentException */ - #[\Override] + #[Override] public function getUser(IteratorFilter $filter): UserModel|null { $iterator = $this->anyDataSet->getIterator($filter); @@ -135,7 +138,7 @@ public function getUser(IteratorFilter $filter): UserModel|null * @throws FileException * @throws InvalidArgumentException */ - #[\Override] + #[Override] public function removeByLoginField(string $login): bool { //anydataset.Row @@ -165,18 +168,21 @@ public function getIterator(IteratorFilter|null $filter = null): IteratorInterfa } /** - * @throws InvalidArgumentException + * @param string $propertyName + * @param string $value + * @return array */ - #[\Override] + #[Override] public function getUsersByProperty(string $propertyName, string $value): array { return $this->getUsersByPropertySet([$propertyName => $value]); } /** - * @throws InvalidArgumentException + * @param array $propertiesArray + * @return array */ - #[\Override] + #[Override] public function getUsersByPropertySet(array $propertiesArray): array { $filter = new IteratorFilter(); @@ -201,7 +207,7 @@ public function getUsersByPropertySet(array $propertiesArray): array * @throws UserExistsException * @throws UserNotFoundException */ - #[\Override] + #[Override] public function addProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value): bool { //anydataset.Row @@ -223,7 +229,7 @@ public function addProperty(string|HexUuidLiteral|int $userId, string $propertyN * @throws UserExistsException * @throws FileException */ - #[\Override] + #[Override] public function setProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value): bool { $user = $this->getById($userId); @@ -245,7 +251,7 @@ public function setProperty(string|HexUuidLiteral|int $userId, string $propertyN * @throws InvalidArgumentException * @throws UserExistsException */ - #[\Override] + #[Override] public function removeProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value = null): bool { $user = $this->getById($userId); @@ -276,10 +282,10 @@ public function removeProperty(string|HexUuidLiteral|int $userId, string $proper * @throws InvalidArgumentException * @throws UserExistsException */ - #[\Override] + #[Override] public function removeAllProperties(string $propertyName, string|null $value = null): void { - $iterator = $this->getIterator(null); + $iterator = $this->getIterator(); foreach ($iterator as $user) { //anydataset.Row $this->removeProperty($user->get($this->getUserDefinition()->getUserid()), $propertyName, $value); @@ -326,7 +332,7 @@ private function createUserModel(RowInterface $row): UserModel * @return bool * @throws InvalidArgumentException */ - #[\Override] + #[Override] public function removeUserById(string|HexUuidLiteral|int $userid): bool { $iteratorFilter = new IteratorFilter(); diff --git a/src/UsersBase.php b/src/UsersBase.php index e286db0..80cfe2c 100644 --- a/src/UsersBase.php +++ b/src/UsersBase.php @@ -16,6 +16,7 @@ use ByJG\JwtWrapper\JwtWrapperException; use ByJG\MicroOrm\Literal\HexUuidLiteral; use ByJG\Serializer\Exception\InvalidArgumentException; +use Override; /** * Base implementation to search and handle users in XMLNuke. @@ -36,7 +37,7 @@ abstract class UsersBase implements UsersInterface /** * @return UserDefinition */ - #[\Override] + #[Override] public function getUserDefinition(): UserDefinition { if ($this->userTable === null) { @@ -48,7 +49,7 @@ public function getUserDefinition(): UserDefinition /** * @return UserPropertiesDefinition */ - #[\Override] + #[Override] public function getUserPropertiesDefinition(): UserPropertiesDefinition { if ($this->propertiesTable === null) { @@ -62,7 +63,7 @@ public function getUserPropertiesDefinition(): UserPropertiesDefinition * * @param UserModel $model */ - #[\Override] + #[Override] abstract public function save(UserModel $model): UserModel; /** @@ -74,7 +75,7 @@ abstract public function save(UserModel $model): UserModel; * @param string $password * @return UserModel */ - #[\Override] + #[Override] public function addUser(string $name, string $userName, string $email, string $password): UserModel { $model = $this->getUserDefinition()->modelInstance(); @@ -92,7 +93,7 @@ public function addUser(string $name, string $userName, string $email, string $p * @throws UserExistsException * @throws InvalidArgumentException */ - #[\Override] + #[Override] public function canAddUser(UserModel $model): bool { if ($this->getByEmail($model->getEmail()) !== null) { @@ -114,7 +115,7 @@ public function canAddUser(UserModel $model): bool * @param IteratorFilter $filter Filter to find user * @return UserModel|null * */ - #[\Override] + #[Override] abstract public function getUser(IteratorFilter $filter): UserModel|null; /** @@ -125,7 +126,7 @@ abstract public function getUser(IteratorFilter $filter): UserModel|null; * @return UserModel|null * @throws InvalidArgumentException */ - #[\Override] + #[Override] public function getByEmail(string $email): UserModel|null { $filter = new IteratorFilter(); @@ -141,7 +142,7 @@ public function getByEmail(string $email): UserModel|null * @return UserModel|null * @throws InvalidArgumentException */ - #[\Override] + #[Override] public function getByUsername(string $username): UserModel|null { $filter = new IteratorFilter(); @@ -156,7 +157,7 @@ public function getByUsername(string $username): UserModel|null * @param string $login * @return UserModel|null */ - #[\Override] + #[Override] public function getByLoginField(string $login): UserModel|null { $filter = new IteratorFilter(); @@ -173,7 +174,7 @@ public function getByLoginField(string $login): UserModel|null * @return UserModel|null * @throws InvalidArgumentException */ - #[\Override] + #[Override] public function getById(string|HexUuidLiteral|int $userid): UserModel|null { $filter = new IteratorFilter(); @@ -187,7 +188,7 @@ public function getById(string|HexUuidLiteral|int $userid): UserModel|null * @param string $login * @return bool * */ - #[\Override] + #[Override] abstract public function removeByLoginField(string $login): bool; /** @@ -199,16 +200,19 @@ abstract public function removeByLoginField(string $login): bool; * @return UserModel|null * @throws InvalidArgumentException */ - #[\Override] + #[Override] public function isValidUser(string $userName, string $password): UserModel|null { $filter = new IteratorFilter(); - $passwordGenerator = $this->getUserDefinition()->getClosureForUpdate(UserDefinition::FIELD_PASSWORD); + $passwordMapper = $this->getUserDefinition()->getMapperForUpdate(UserDefinition::FIELD_PASSWORD); + if (is_string($passwordMapper)) { + $passwordMapper = new $passwordMapper(); + } $filter->and($this->getUserDefinition()->loginField(), Relation::EQUAL, strtolower($userName)); $filter->and( $this->getUserDefinition()->getPassword(), Relation::EQUAL, - $passwordGenerator($password, null) + $passwordMapper->processedValue($password, null) ); return $this->getUser($filter); } @@ -224,7 +228,7 @@ public function isValidUser(string $userName, string $password): UserModel|null * @throws UserNotFoundException * @throws InvalidArgumentException */ - #[\Override] + #[Override] public function hasProperty(string|int|HexUuidLiteral|null $userId, string $propertyName, string|null $value = null): bool { //anydataset.Row @@ -261,7 +265,7 @@ public function hasProperty(string|int|HexUuidLiteral|null $userId, string $prop * @throws UserNotFoundException * @throws InvalidArgumentException */ - #[\Override] + #[Override] public function getProperty(string|HexUuidLiteral|int $userId, string $propertyName): array|string|UserPropertiesModel|null { $user = $this->getById($userId); @@ -288,7 +292,7 @@ abstract public function getUsersByPropertySet(array $propertiesArray): array; * @param string $propertyName * @param string|null $value */ - #[\Override] + #[Override] abstract public function addProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value): bool; /** @@ -300,7 +304,7 @@ abstract public function addProperty(string|HexUuidLiteral|int $userId, string $ * @param string|null $value Property value with a site * @return bool * */ - #[\Override] + #[Override] abstract public function removeProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value = null): bool; /** @@ -311,7 +315,7 @@ abstract public function removeProperty(string|HexUuidLiteral|int $userId, strin * @param string|null $value Property value with a site * @return void * */ - #[\Override] + #[Override] abstract public function removeAllProperties(string $propertyName, string|null $value = null): void; /** @@ -320,7 +324,7 @@ abstract public function removeAllProperties(string $propertyName, string|null $ * @throws UserNotFoundException * @throws InvalidArgumentException */ - #[\Override] + #[Override] public function isAdmin(string|HexUuidLiteral|int $userId): bool { $user = $this->getById($userId); @@ -347,7 +351,7 @@ public function isAdmin(string|HexUuidLiteral|int $userId): bool * @throws UserNotFoundException * @throws InvalidArgumentException */ - #[\Override] + #[Override] public function createAuthToken( string $login, string $password, @@ -392,7 +396,7 @@ public function createAuthToken( * @throws NotAuthenticatedException * @throws UserNotFoundException */ - #[\Override] + #[Override] public function isValidToken(string $login, JwtWrapper $jwtWrapper, string $token): array|null { $user = $this->getByLoginField($login); @@ -418,6 +422,6 @@ public function isValidToken(string $login, JwtWrapper $jwtWrapper, string $toke /** * @param string|int|HexUuidLiteral $userid */ - #[\Override] + #[Override] abstract public function removeUserById(string|HexUuidLiteral|int $userid): bool; } diff --git a/src/UsersDBDataset.php b/src/UsersDBDataset.php index d502fe2..e07d872 100644 --- a/src/UsersDBDataset.php +++ b/src/UsersDBDataset.php @@ -2,9 +2,11 @@ namespace ByJG\Authenticate; +use ByJG\AnyDataset\Core\Exception\DatabaseException; use ByJG\AnyDataset\Core\IteratorFilter; use ByJG\AnyDataset\Db\DatabaseExecutor; use ByJG\AnyDataset\Db\DbDriverInterface; +use ByJG\AnyDataset\Db\Exception\DbDriverNotConnected; use ByJG\AnyDataset\Db\IteratorFilterSqlFormatter; use ByJG\Authenticate\Definition\UserDefinition; use ByJG\Authenticate\Definition\UserPropertiesDefinition; @@ -25,7 +27,10 @@ use ByJG\MicroOrm\Query; use ByJG\MicroOrm\Repository; use ByJG\Serializer\Exception\InvalidArgumentException; +use ByJG\XmlUtil\Exception\FileException; +use ByJG\XmlUtil\Exception\XmlUtilException; use Exception; +use Override; use ReflectionException; class UsersDBDataset extends UsersBase @@ -55,6 +60,8 @@ class UsersDBDataset extends UsersBase * * @throws OrmModelInvalidException * @throws ReflectionException + * @throws ExceptionInvalidArgumentException + * @throws ExceptionInvalidArgumentException */ public function __construct( DbDriverInterface|DatabaseExecutor $dbDriver, @@ -80,7 +87,7 @@ public function __construct( $userTable->table(), $userTable->getUserid() ); - $seed = $userTable->getGenerateKeyClosure(); + $seed = $userTable->getGenerateKey(); if (!empty($seed)) { $userMapper->withPrimaryKeySeedFunction($seed); } @@ -126,7 +133,7 @@ public function __construct( * @throws OrmInvalidFieldsException * @throws Exception */ - #[\Override] + #[Override] public function save(UserModel $model): UserModel { $newUser = false; @@ -160,6 +167,11 @@ public function save(UserModel $model): UserModel * * @param IteratorFilter|null $filter Filter to find user * @return UserModel[] + * @throws DatabaseException + * @throws DbDriverNotConnected + * @throws FileException + * @throws XmlUtilException + * @throws \Psr\SimpleCache\InvalidArgumentException */ public function getIterator(IteratorFilter|null $filter = null): array { @@ -184,8 +196,14 @@ public function getIterator(IteratorFilter|null $filter = null): array * * @param IteratorFilter $filter Filter to find user * @return UserModel|null + * @throws DatabaseException + * @throws DbDriverNotConnected + * @throws FileException + * @throws RepositoryReadOnlyException + * @throws XmlUtilException + * @throws \Psr\SimpleCache\InvalidArgumentException */ - #[\Override] + #[Override] public function getUser(IteratorFilter $filter): UserModel|null { $result = $this->getIterator($filter); @@ -207,7 +225,7 @@ public function getUser(IteratorFilter $filter): UserModel|null * @return bool * @throws Exception */ - #[\Override] + #[Override] public function removeByLoginField(string $login): bool { $user = $this->getByLoginField($login); @@ -226,7 +244,7 @@ public function removeByLoginField(string $login): bool * @return bool * @throws Exception */ - #[\Override] + #[Override] public function removeUserById(string|HexUuidLiteral|int $userid): bool { $updateTableProperties = DeleteQuery::getInstance() @@ -250,10 +268,15 @@ public function removeUserById(string|HexUuidLiteral|int $userid): bool * @param string $propertyName * @param string $value * @return array - * @throws InvalidArgumentException + * @throws DatabaseException + * @throws DbDriverNotConnected * @throws ExceptionInvalidArgumentException + * @throws FileException + * @throws InvalidArgumentException + * @throws XmlUtilException + * @throws \Psr\SimpleCache\InvalidArgumentException */ - #[\Override] + #[Override] public function getUsersByProperty(string $propertyName, string $value): array { return $this->getUsersByPropertySet([$propertyName => $value]); @@ -264,10 +287,15 @@ public function getUsersByProperty(string $propertyName, string $value): array * * @param array $propertiesArray * @return array - * @throws InvalidArgumentException + * @throws DatabaseException + * @throws DbDriverNotConnected * @throws ExceptionInvalidArgumentException + * @throws FileException + * @throws InvalidArgumentException + * @throws XmlUtilException + * @throws \Psr\SimpleCache\InvalidArgumentException */ - #[\Override] + #[Override] public function getUsersByPropertySet(array $propertiesArray): array { $query = Query::getInstance() @@ -295,7 +323,7 @@ public function getUsersByPropertySet(array $propertiesArray): array * @throws OrmInvalidFieldsException * @throws Exception */ - #[\Override] + #[Override] public function addProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value): bool { //anydataset.Row @@ -314,14 +342,22 @@ public function addProperty(string|HexUuidLiteral|int $userId, string $propertyN } /** - * @throws UpdateConstraintException - * @throws RepositoryReadOnlyException - * @throws InvalidArgumentException - * @throws OrmBeforeInvalidException + * @param string|HexUuidLiteral|int $userId + * @param string $propertyName + * @param string|null $value + * @return bool + * @throws DatabaseException + * @throws DbDriverNotConnected * @throws ExceptionInvalidArgumentException + * @throws FileException + * @throws OrmBeforeInvalidException * @throws OrmInvalidFieldsException + * @throws RepositoryReadOnlyException + * @throws UpdateConstraintException + * @throws XmlUtilException + * @throws \Psr\SimpleCache\InvalidArgumentException */ - #[\Override] + #[Override] public function setProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value): bool { $query = Query::getInstance() @@ -351,11 +387,13 @@ public function setProperty(string|HexUuidLiteral|int $userId, string $propertyN * @param string $propertyName Property name * @param string|null $value Property value with a site * @return bool + * @throws DatabaseException + * @throws DbDriverNotConnected * @throws ExceptionInvalidArgumentException * @throws InvalidArgumentException * @throws RepositoryReadOnlyException */ - #[\Override] + #[Override] public function removeProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value = null): bool { $user = $this->getById($userId); @@ -385,10 +423,12 @@ public function removeProperty(string|HexUuidLiteral|int $userId, string $proper * @param string $propertyName Property name * @param string|null $value Property value with a site * @return void + * @throws DatabaseException + * @throws DbDriverNotConnected * @throws ExceptionInvalidArgumentException * @throws RepositoryReadOnlyException */ - #[\Override] + #[Override] public function removeAllProperties(string $propertyName, string|null $value = null): void { $updateable = DeleteQuery::getInstance() @@ -402,7 +442,14 @@ public function removeAllProperties(string $propertyName, string|null $value = n $this->propertiesRepository->deleteByQuery($updateable); } - #[\Override] + /** + * @throws XmlUtilException + * @throws DatabaseException + * @throws DbDriverNotConnected + * @throws FileException + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + #[Override] public function getProperty(string|HexUuidLiteral|int $userId, string $propertyName): array|string|UserPropertiesModel|null { $query = Query::getInstance() @@ -430,7 +477,12 @@ public function getProperty(string|HexUuidLiteral|int $userId, string $propertyN * Return all property's fields from this user * * @param UserModel $userRow + * @throws DatabaseException + * @throws DbDriverNotConnected + * @throws FileException * @throws RepositoryReadOnlyException + * @throws XmlUtilException + * @throws \Psr\SimpleCache\InvalidArgumentException */ protected function setPropertiesInUser(UserModel $userRow): void { diff --git a/tests/UsersDBDatasetDefinitionTest.php b/tests/UsersDBDatasetDefinitionTest.php index 82c05d6..443d35a 100644 --- a/tests/UsersDBDatasetDefinitionTest.php +++ b/tests/UsersDBDatasetDefinitionTest.php @@ -3,6 +3,7 @@ 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; @@ -13,6 +14,8 @@ use ByJG\MicroOrm\Exception\OrmBeforeInvalidException; use ByJG\MicroOrm\Exception\OrmInvalidFieldsException; use ByJG\MicroOrm\Exception\OrmModelInvalidException; +use ByJG\MicroOrm\Interface\UniqueIdGeneratorInterface; +use ByJG\MicroOrm\Literal\Literal; use Exception; use ReflectionException; @@ -37,6 +40,21 @@ public function setOtherfield($otherfield) } } +class TestUniqueIdGenerator implements UniqueIdGeneratorInterface +{ + private string $prefix; + + public function __construct(string $prefix = 'TEST-') + { + $this->prefix = $prefix; + } + + public function process(DatabaseExecutor $executor, array|object $instance): string|Literal|int + { + return $this->prefix . uniqid(); + } +} + class UsersDBDatasetDefinitionTest extends UsersDBDatasetByUsernameTest { protected $db; @@ -216,4 +234,85 @@ public function testWithUpdateValue() $this->assertEquals(']*other john*[', $user->getOtherfield()); $this->assertEquals('2017-12-04 00:00:00', $user->getCreated()); } + + /** + * @throws Exception + */ + public function testDefineGenerateKeyWithInterface() + { + // Create a separate table with varchar userid for testing custom generators + $this->db->execute('create table users_custom ( + userid varchar(50) primary key, + name varchar(45), + email varchar(200), + username varchar(20), + password varchar(40), + created datetime default (datetime(\'2017-12-04\')), + admin char(1));' + ); + + // Create a new user definition with custom generator + $userDefinition = new UserDefinition('users_custom', UserModel::class, UserDefinition::LOGIN_IS_USERNAME); + $generator = new TestUniqueIdGenerator('CUSTOM-'); + $userDefinition->defineGenerateKey($generator); + + // Create dataset with custom definition + $dataset = new UsersDBDataset($this->db, $userDefinition, $this->propertyDefinition); + + // Add a user - the custom generator should be used + $user = $dataset->addUser('Test User', 'testuser', 'test@example.com', 'password123'); + + // Verify the user ID was generated with the custom prefix + $this->assertStringStartsWith('CUSTOM-', $user->getUserid()); + $this->assertEquals('Test User', $user->getName()); + $this->assertEquals('testuser', $user->getUsername()); + + // Cleanup + $this->db->execute('drop table users_custom'); + } + + /** + * @throws Exception + */ + public function testDefineGenerateKeyWithString() + { + // Create a separate table with varchar userid for testing custom generators + $this->db->execute('create table users_custom2 ( + userid varchar(50) primary key, + name varchar(45), + email varchar(200), + username varchar(20), + password varchar(40), + created datetime default (datetime(\'2017-12-04\')), + admin char(1));' + ); + + // Create a new user definition with generator class string + $userDefinition = new UserDefinition('users_custom2', UserModel::class, UserDefinition::LOGIN_IS_USERNAME); + $userDefinition->defineGenerateKey(TestUniqueIdGenerator::class); + + // Create dataset with custom definition + $dataset = new UsersDBDataset($this->db, $userDefinition, $this->propertyDefinition); + + // Add a user - the custom generator should be instantiated and used + $user = $dataset->addUser('Test User 2', 'testuser2', 'test2@example.com', 'password123'); + + // Verify the user ID was generated with the default TEST- prefix + $this->assertStringStartsWith('TEST-', $user->getUserid()); + $this->assertEquals('Test User 2', $user->getName()); + + // Cleanup + $this->db->execute('drop table users_custom2'); + } + + public function testDefineGenerateKeyClosureThrowsException() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('defineGenerateKeyClosure is deprecated. Use defineGenerateKey with UniqueIdGeneratorInterface instead.'); + + $userDefinition = new UserDefinition(); + $userDefinition->defineGenerateKeyClosure(function ($executor, $instance) { + return 'test-id'; + }); + } } From a2ee8f0402729547766c672c731d808d1baf295a Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Sat, 1 Nov 2025 16:12:07 -0400 Subject: [PATCH 04/40] Add `PasswordMd5MapperTest` to verify MD5 hashing behavior, ensure passwords are hashed or preserved correctly during save and update operations, and validate user login functionality. --- .claude/settings.local.json | 9 -- .gitignore | 1 + tests/PasswordMd5MapperTest.php | 219 ++++++++++++++++++++++++++++++++ 3 files changed, 220 insertions(+), 9 deletions(-) delete mode 100644 .claude/settings.local.json create mode 100644 tests/PasswordMd5MapperTest.php diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 46507b9..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(find:*)" - ], - "deny": [], - "ask": [] - } -} 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/tests/PasswordMd5MapperTest.php b/tests/PasswordMd5MapperTest.php new file mode 100644 index 0000000..ab35190 --- /dev/null +++ b/tests/PasswordMd5MapperTest.php @@ -0,0 +1,219 @@ +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));' + ); + + // Create user definition with custom MD5 password mapper + $this->userDefinition = new UserDefinition('users', UserModel::class, UserDefinition::LOGIN_IS_USERNAME); + $this->userDefinition->defineMapperForUpdate(UserDefinition::FIELD_PASSWORD, PasswordMd5Mapper::class); + $this->userDefinition->markPropertyAsReadOnly(UserDefinition::FIELD_CREATED); + + $this->propertyDefinition = new UserPropertiesDefinition(); + } + + public function tearDown(): void + { + $uri = new Uri(self::CONNECTION_STRING); + if (file_exists($uri->getPath())) { + unlink($uri->getPath()); + } + $this->db = null; + $this->userDefinition = null; + $this->propertyDefinition = null; + } + + public function testPasswordIsHashedWithMd5OnSave() + { + $dataset = new UsersDBDataset($this->db, $this->userDefinition, $this->propertyDefinition); + + // Add a user with a plain text password + $plainPassword = 'mySecretPassword123'; + $user = $dataset->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() + { + $dataset = new UsersDBDataset($this->db, $this->userDefinition, $this->propertyDefinition); + + // Create a user + $user = $dataset->addUser('Jane Doe', 'janedoe', 'jane@example.com', 'password123'); + $originalHash = $user->getPassword(); + + // Update the user without changing password + $user->setName('Jane Smith'); + $updatedUser = $dataset->save($user); + + // Password hash should remain the same + $this->assertEquals($originalHash, $updatedUser->getPassword()); + } + + public function testPasswordIsHashedWhenUpdating() + { + $dataset = new UsersDBDataset($this->db, $this->userDefinition, $this->propertyDefinition); + + // Create a user + $user = $dataset->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 = $dataset->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 = $dataset->isValidUser('janedoe', $newPlainPassword); + $this->assertNotNull($authenticatedUser); + + // Verify user cannot login with old password + $authenticatedUserOld = $dataset->isValidUser('janedoe', 'oldPassword'); + $this->assertNull($authenticatedUserOld); + } + + public function testPasswordRemainsUnchangedWhenUpdatingOtherFields() + { + $dataset = new UsersDBDataset($this->db, $this->userDefinition, $this->propertyDefinition); + + // Create a user + $originalPassword = 'myPassword123'; + $user = $dataset->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->setAdmin('y'); + $updatedUser = $dataset->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('y', $updatedUser->getAdmin()); + + // Verify user can still login with original password + $authenticatedUser = $dataset->isValidUser('johnsmith', $originalPassword); + $this->assertNotNull($authenticatedUser); + $this->assertEquals('John Updated', $authenticatedUser->getName()); + } + + public function testUserCanLoginWithMd5HashedPassword() + { + $dataset = new UsersDBDataset($this->db, $this->userDefinition, $this->propertyDefinition); + + // Add a user + $plainPassword = 'testPassword456'; + $dataset->addUser('Test User', 'testuser', 'test@example.com', $plainPassword); + + // Verify user can login with the plain text password + $authenticatedUser = $dataset->isValidUser('testuser', $plainPassword); + + $this->assertNotNull($authenticatedUser); + $this->assertEquals('Test User', $authenticatedUser->getName()); + $this->assertEquals('testuser', $authenticatedUser->getUsername()); + } + + public function testUserCannotLoginWithWrongPassword() + { + $dataset = new UsersDBDataset($this->db, $this->userDefinition, $this->propertyDefinition); + + // Add a user + $dataset->addUser('Test User', 'testuser', 'test@example.com', 'correctPassword'); + + // Try to login with wrong password + $authenticatedUser = $dataset->isValidUser('testuser', 'wrongPassword'); + + $this->assertNull($authenticatedUser); + } + + public function testEmptyPasswordReturnsNull() + { + $mapper = new PasswordMd5Mapper(); + + $this->assertNull($mapper->processedValue('', null)); + $this->assertNull($mapper->processedValue(null, null)); + } + + public function testExistingMd5HashIsNotRehashed() + { + $mapper = new PasswordMd5Mapper(); + $existingHash = '5f4dcc3b5aa765d61d8327deb882cf99'; // MD5 of 'password' + + $result = $mapper->processedValue($existingHash, null); + + $this->assertEquals($existingHash, $result); + } +} From c6e09defa993840abb369e90a078abd1ca1032bc Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Sat, 1 Nov 2025 17:35:31 -0400 Subject: [PATCH 05/40] Change parameter type order in `UsersDBDataset` constructor to `DatabaseExecutor|DbDriverInterface` for improved type clarity. --- src/UsersDBDataset.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UsersDBDataset.php b/src/UsersDBDataset.php index e07d872..7d32ef5 100644 --- a/src/UsersDBDataset.php +++ b/src/UsersDBDataset.php @@ -64,7 +64,7 @@ class UsersDBDataset extends UsersBase * @throws ExceptionInvalidArgumentException */ public function __construct( - DbDriverInterface|DatabaseExecutor $dbDriver, + DatabaseExecutor|DbDriverInterface $dbDriver, UserDefinition|null $userTable = null, UserPropertiesDefinition|null $propertiesTable = null ) { From b3e8bcd5deaab92db6b42ec49694744534be1e7c Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Sun, 2 Nov 2025 15:06:36 -0500 Subject: [PATCH 06/40] Migrate from `UniqueIdGeneratorInterface` to `MapperFunctionInterface` for custom key generation, update related methods and tests, replace deprecated messages, enhance type safety, and align with new mapper conventions. --- src/Definition/UserDefinition.php | 11 +++++------ src/MapperFunctions/PasswordSha1Mapper.php | 4 ++-- src/MapperFunctions/UserIdGeneratorMapper.php | 4 ++-- src/UsersDBDataset.php | 2 +- tests/UsersDBDatasetDefinitionTest.php | 19 ++++++++++--------- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/Definition/UserDefinition.php b/src/Definition/UserDefinition.php index 1cd3939..df16fad 100644 --- a/src/Definition/UserDefinition.php +++ b/src/Definition/UserDefinition.php @@ -9,7 +9,6 @@ use ByJG\Authenticate\Model\UserModel; use ByJG\MicroOrm\Interface\EntityProcessorInterface; use ByJG\MicroOrm\Interface\MapperFunctionInterface; -use ByJG\MicroOrm\Interface\UniqueIdGeneratorInterface; use ByJG\MicroOrm\MapperFunctions\ReadOnlyMapper; use ByJG\MicroOrm\MapperFunctions\StandardMapper; use ByJG\Serializer\Serialize; @@ -26,7 +25,7 @@ class UserDefinition protected string $__loginField; protected string $__model; protected array $__properties = []; - protected UniqueIdGeneratorInterface|string|null $__generateKey = null; + protected MapperFunctionInterface|string|null $__generateKey = null; const FIELD_USERID = 'userid'; const FIELD_NAME = 'name'; @@ -230,15 +229,15 @@ public function getClosureForUpdate(string $property): Closure */ public function defineGenerateKeyClosure(Closure $closure): void { - throw new InvalidArgumentException('defineGenerateKeyClosure is deprecated. Use defineGenerateKey with UniqueIdGeneratorInterface instead.'); + throw new InvalidArgumentException('defineGenerateKeyClosure is deprecated. Use defineGenerateKey with MapperFunctionsInterface instead.'); } - public function defineGenerateKey(UniqueIdGeneratorInterface|string $generator): void + public function defineGenerateKey(MapperFunctionInterface|string $generator): void { $this->__generateKey = $generator; } - public function getGenerateKey(): UniqueIdGeneratorInterface|string|null + public function getGenerateKey(): MapperFunctionInterface|string|null { return $this->__generateKey; } @@ -246,7 +245,7 @@ public function getGenerateKey(): UniqueIdGeneratorInterface|string|null /** * @deprecated Use getGenerateKey instead */ - public function getGenerateKeyClosure(): UniqueIdGeneratorInterface|string|null + public function getGenerateKeyClosure(): MapperFunctionInterface|string|null { return $this->__generateKey; } diff --git a/src/MapperFunctions/PasswordSha1Mapper.php b/src/MapperFunctions/PasswordSha1Mapper.php index d995ff5..30ae0e0 100644 --- a/src/MapperFunctions/PasswordSha1Mapper.php +++ b/src/MapperFunctions/PasswordSha1Mapper.php @@ -2,7 +2,7 @@ namespace ByJG\Authenticate\MapperFunctions; -use ByJG\AnyDataset\Db\DbFunctionsInterface; +use ByJG\AnyDataset\Db\DatabaseExecutor; use ByJG\MicroOrm\Interface\MapperFunctionInterface; /** @@ -10,7 +10,7 @@ */ class PasswordSha1Mapper implements MapperFunctionInterface { - public function processedValue(mixed $value, mixed $instance, ?DbFunctionsInterface $helper = null): mixed + public function processedValue(mixed $value, mixed $instance, ?DatabaseExecutor $executor = null): mixed { // Already have a SHA1 password (40 characters) if (is_string($value) && strlen($value) === 40) { diff --git a/src/MapperFunctions/UserIdGeneratorMapper.php b/src/MapperFunctions/UserIdGeneratorMapper.php index 4043550..d28d1e6 100644 --- a/src/MapperFunctions/UserIdGeneratorMapper.php +++ b/src/MapperFunctions/UserIdGeneratorMapper.php @@ -2,7 +2,7 @@ namespace ByJG\Authenticate\MapperFunctions; -use ByJG\AnyDataset\Db\DbFunctionsInterface; +use ByJG\AnyDataset\Db\DatabaseExecutor; use ByJG\Authenticate\Model\UserModel; use ByJG\MicroOrm\Interface\MapperFunctionInterface; @@ -11,7 +11,7 @@ */ class UserIdGeneratorMapper implements MapperFunctionInterface { - public function processedValue(mixed $value, mixed $instance, ?DbFunctionsInterface $helper = null): mixed + public function processedValue(mixed $value, mixed $instance, ?DatabaseExecutor $executor = null): mixed { // If value is already set, use it if (!empty($value)) { diff --git a/src/UsersDBDataset.php b/src/UsersDBDataset.php index 7d32ef5..f3cce7b 100644 --- a/src/UsersDBDataset.php +++ b/src/UsersDBDataset.php @@ -489,7 +489,7 @@ protected function setPropertiesInUser(UserModel $userRow): void $value = $this->propertiesRepository ->getMapper() ->getFieldMap(UserDefinition::FIELD_USERID) - ->getUpdateFunctionValue($userRow->getUserid(), $userRow, $this->propertiesRepository->getExecutorWrite()->getHelper()); + ->getUpdateFunctionValue($userRow->getUserid(), $userRow, $this->propertiesRepository->getExecutorWrite()); $query = Query::getInstance() ->table($this->getUserPropertiesDefinition()->table()) ->where("{$this->getUserPropertiesDefinition()->getUserid()} = :id", ['id' => $value]); diff --git a/tests/UsersDBDatasetDefinitionTest.php b/tests/UsersDBDatasetDefinitionTest.php index 443d35a..cdff5b8 100644 --- a/tests/UsersDBDatasetDefinitionTest.php +++ b/tests/UsersDBDatasetDefinitionTest.php @@ -14,9 +14,9 @@ use ByJG\MicroOrm\Exception\OrmBeforeInvalidException; use ByJG\MicroOrm\Exception\OrmInvalidFieldsException; use ByJG\MicroOrm\Exception\OrmModelInvalidException; -use ByJG\MicroOrm\Interface\UniqueIdGeneratorInterface; -use ByJG\MicroOrm\Literal\Literal; +use ByJG\MicroOrm\Interface\MapperFunctionInterface; use Exception; +use Override; use ReflectionException; class MyUserModel extends UserModel @@ -40,7 +40,7 @@ public function setOtherfield($otherfield) } } -class TestUniqueIdGenerator implements UniqueIdGeneratorInterface +class TestUniqueIdGenerator implements MapperFunctionInterface { private string $prefix; @@ -49,7 +49,8 @@ public function __construct(string $prefix = 'TEST-') $this->prefix = $prefix; } - public function process(DatabaseExecutor $executor, array|object $instance): string|Literal|int + #[Override] + public function processedValue(mixed $value, mixed $instance, ?DatabaseExecutor $executor = null): mixed { return $this->prefix . uniqid(); } @@ -67,7 +68,7 @@ class UsersDBDatasetDefinitionTest extends UsersDBDatasetByUsernameTest * @throws UserExistsException * @throws ReflectionException */ - #[\Override] + #[Override] public function __setUp($loginField) { $this->prefix = ""; @@ -133,7 +134,7 @@ public function __setUp($loginField) * @throws ReflectionException * @throws UserExistsException */ - #[\Override] + #[Override] public function setUp(): void { $this->__setUp(UserDefinition::LOGIN_IS_USERNAME); @@ -144,7 +145,7 @@ public function setUp(): void * @throws DatabaseException * @throws \ByJG\Serializer\Exception\InvalidArgumentException */ - #[\Override] + #[Override] public function testAddUser() { $this->object->save(new MyUserModel('John Doe', 'johndoe@gmail.com', 'john', 'mypassword', 'no', 'other john')); @@ -173,7 +174,7 @@ public function testAddUser() /** * @throws Exception */ - #[\Override] + #[Override] public function testWithUpdateValue() { // For Update Definitions @@ -308,7 +309,7 @@ public function testDefineGenerateKeyWithString() public function testDefineGenerateKeyClosureThrowsException() { $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('defineGenerateKeyClosure is deprecated. Use defineGenerateKey with UniqueIdGeneratorInterface instead.'); + $this->expectExceptionMessage('defineGenerateKeyClosure is deprecated. Use defineGenerateKey with MapperFunctionsInterface instead.'); $userDefinition = new UserDefinition(); $userDefinition->defineGenerateKeyClosure(function ($executor, $instance) { From 95ced77a57a2e96ea2934833a3e3f2ed2eeda947 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Nov 2025 20:33:54 +0000 Subject: [PATCH 07/40] Add comprehensive Docusaurus documentation and update README - Created comprehensive documentation in /docs folder with 12 markdown files - Added sidebar_position to all docs following the order in README.md - Fixed inaccurate code examples in README.md to match current implementation - Updated README with links to all documentation sections - Fixed example.php to use correct API (UsersAnyDataset constructor, SessionContext) - Organized docs with proper Docusaurus frontmatter and features - Added detailed guides for: * Getting started and installation * User management (CRUD operations) * Authentication methods (session and JWT) * Session context and storage options * User properties (custom key-value data) * Database storage and schema customization * Password validation and generation * JWT token authentication * Custom fields and extending UserModel * Mappers and entity processors * Complete working examples - Removed duplicate and outdated information from README - Added feature list and improved organization - All code examples now reflect actual API usage --- README.md | 419 ++++++++++++++--------------- docs/authentication.md | 150 +++++++++++ docs/custom-fields.md | 375 ++++++++++++++++++++++++++ docs/database-storage.md | 256 ++++++++++++++++++ docs/examples.md | 520 ++++++++++++++++++++++++++++++++++++ docs/getting-started.md | 58 ++++ docs/installation.md | 51 ++++ docs/jwt-tokens.md | 382 ++++++++++++++++++++++++++ docs/mappers.md | 433 ++++++++++++++++++++++++++++++ docs/password-validation.md | 288 ++++++++++++++++++++ docs/session-context.md | 197 ++++++++++++++ docs/user-management.md | 154 +++++++++++ docs/user-properties.md | 248 +++++++++++++++++ example.php | 43 ++- 14 files changed, 3347 insertions(+), 227 deletions(-) create mode 100644 docs/authentication.md create mode 100644 docs/custom-fields.md create mode 100644 docs/database-storage.md create mode 100644 docs/examples.md create mode 100644 docs/getting-started.md create mode 100644 docs/installation.md create mode 100644 docs/jwt-tokens.md create mode 100644 docs/mappers.md create mode 100644 docs/password-validation.md create mode 100644 docs/session-context.md create mode 100644 docs/user-management.md create mode 100644 docs/user-properties.md diff --git a/README.md b/README.md index a16b060..b973e33 100644 --- a/README.md +++ b/README.md @@ -6,297 +6,258 @@ [![GitHub license](https://img.shields.io/github/license/byjg/php-authuser.svg)](https://opensource.byjg.com/opensource/licensing.html) [![GitHub release](https://img.shields.io/github/release/byjg/php-authuser.svg)](https://github.com/byjg/php-authuser/releases/) -A simple and customizable class for enable user authentication inside your application. It is available on XML files, Relational Databases. +A simple and customizable library for user authentication in PHP applications. It supports multiple storage backends including databases and XML files. -The main purpose is just to handle all complexity of validate a user, add properties and create access token abstracting the database layer. -This class can persist into session (or file, memcache, etc) the user data between requests. +The main purpose is to handle all complexity of user validation, authentication, properties management, and access tokens, abstracting the database layer. +This class can persist user data into session (or file, memcache, etc.) between requests. -## Creating a Users handling class +## Documentation -Using the FileSystem (XML) as the user storage: +- [Getting Started](docs/getting-started.md) +- [Installation](docs/installation.md) +- [User Management](docs/user-management.md) +- [Authentication](docs/authentication.md) +- [Session Context](docs/session-context.md) +- [User Properties](docs/user-properties.md) +- [Database Storage](docs/database-storage.md) +- [Password Validation](docs/password-validation.md) +- [JWT Tokens](docs/jwt-tokens.md) +- [Custom Fields](docs/custom-fields.md) +- [Mappers](docs/mappers.md) +- [Examples](docs/examples.md) -```php -isAuthenticated()) { - - // Get the userId of the authenticated users - $userId = $sessionContext->userInfo(); +// Create or load AnyDataset +$anyDataset = new AnyDataset('/tmp/users.xml'); - // Get the user and your name - $user = $users->getById($userId); - echo "Hello: " . $user->getName(); -} +// Initialize user management +$users = new UsersAnyDataset($anyDataset); ``` -## Saving extra info into the user session +*Note*: See the [AnyDataset DB project](https://github.com/byjg/anydataset-db) for available databases and connection strings. -You can save data in the session data exists only during the user is logged in. Once the user logged off the -data stored with the user session will be released. +## Basic Usage -Store the data for the current user session: +### Creating and Authenticating Users ```php setSessionData('key', 'value'); -``` +use ByJG\Authenticate\SessionContext; +use ByJG\Cache\Factory; -Getting the data from the current user session: +// Add a new user +$user = $users->addUser('John Doe', 'johndoe', 'john@example.com', 'SecurePass123'); -```php -getSessionData('key'); -``` +// Validate user credentials +$authenticatedUser = $users->isValidUser('johndoe', 'SecurePass123'); -Note: If the user is not logged an error will be throw +if ($authenticatedUser !== null) { + // Create session context + $sessionContext = new SessionContext(Factory::createSessionPool()); -## Adding a custom property to the users + // Register the login + $sessionContext->registerLogin($authenticatedUser->getUserid()); -```php -getById($userId); -$user->setField('somefield', 'somevalue'); -$users->save(); + echo "Welcome, " . $authenticatedUser->getName(); +} ``` -## Logout from a session +### Check if User is Authenticated ```php registerLogout(); -``` +$sessionContext = new SessionContext(Factory::createSessionPool()); -## Important note about SessionContext +// Check if the user is authenticated +if ($sessionContext->isAuthenticated()) { + // Get the userId of the authenticated user + $userId = $sessionContext->userInfo(); -`SessionContext` object will store the info about the current context. -As SessionContext uses CachePool interface defined in PSR-6 you can set any storage -to save your session context. + // Get the user and display name + $user = $users->getById($userId); + echo "Hello: " . $user->getName(); +} else { + echo "Please log in"; +} +``` -In our examples we are using a regular PHP Session for store the user context -(`Factory::createSessionPool()`). But if you are using another store like MemCached -you have to define a UNIQUE prefix for that session. Note if TWO users have the same -prefix you probably have an unexpected result for the SessionContext. +## Managing Session Data -Example for memcached: +You can store temporary data in the user session that exists only while the user is logged in. Once the user logs out, the data is automatically released. + +### Store Session Data ```php setSessionData('shopping_cart', [ + 'item1' => 'Product A', + 'item2' => 'Product B' +]); -## Architecture - -```text - ┌───────────────────┐ - │ SessionContext │ - └───────────────────┘ - │ -┌────────────────────────┐ ┌────────────────────────┐ -│ UserDefinition │─ ─ ┐ │ ─ ─ ┤ UserModel │ -└────────────────────────┘ ┌───────────────────┐ │ └────────────────────────┘ -┌────────────────────────┐ └────│ UsersInterface │────┐ ┌────────────────────────┐ -│ UserPropertyDefinition │─ ─ ┘ └───────────────────┘ ─ ─ ┤ UserPropertyModel │ -└────────────────────────┘ ▲ └────────────────────────┘ - │ - ┌────────────────────────┼─────────────────────────┐ - │ │ │ - │ │ │ - │ │ │ - ┌───────────────────┐ ┌───────────────────┐ ┌────────────────────┐ - │ UsersAnyDataset │ │ UsersDBDataset │ │ xxxxxxxxxxxxxxxxxx │ - └───────────────────┘ └───────────────────┘ └────────────────────┘ +$sessionContext->setSessionData('last_page', '/products'); ``` -- UserInterface contain the basic interface for the concrete implementation -- UsersDBDataset is a concrete implementation to retrieve/save user in a Database -- UserAnyDataset is a concrete implementation to retrieve/save user in a Xml file -- UserModel is the basic model get/set for the user -- UserPropertyModel is the basic model get/set for extra user property -- UserDefinition will map the model to the database - -### Database +### Retrieve Session Data -The default structure adopted for store the user data in the database through the -UsersDBDataset class is the follow: +```php +getSessionData('shopping_cart'); +$lastPage = $sessionContext->getSessionData('last_page'); ``` -Using the database structure above you can create the UsersDBDatase as follow: +:::note +A `NotAuthenticatedException` will be thrown if the user is not authenticated when accessing session data. +::: -```php - 'fieldname of userid', - UserDefinition::FIELD_NAME => 'fieldname of name', - UserDefinition::FIELD_EMAIL => 'fieldname of email', - UserDefinition::FIELD_USERNAME => 'fieldname of username', - UserDefinition::FIELD_PASSWORD => 'fieldname of password', - UserDefinition::FIELD_CREATED => 'fieldname of created', - UserDefinition::FIELD_ADMIN => 'fieldname of admin' - ] -); +// Add a property to a user +$users->addProperty($userId, 'phone', '555-1234'); +$users->addProperty($userId, 'department', 'Engineering'); + +// Users can have multiple values for the same property +$users->addProperty($userId, 'role', 'developer'); +$users->addProperty($userId, 'role', 'manager'); ``` -### Adding custom modifiers for read and update +### Using UserModel ```php the current value to be updated -// $instance -> The array with all other fields; -$userDefinition->defineClosureForUpdate(UserDefinition::FIELD_PASSWORD, function ($value, $instance) { - return strtoupper(sha1($value)); -}); - -// Defines a custom function to be applied After the field UserDefinition::FIELD_CREATED is read but before -// the user get the result -// $value --> the current value retrieved from database -// $instance -> The array with all other fields; -$userDefinition->defineClosureForSelect(UserDefinition::FIELD_CREATED, function ($value, $instance) { - return date('Y', $value); -}); - -// If you want make the field READONLY just do it: -$userDefinition->markPropertyAsReadOnly(UserDefinition::FIELD_CREATED); -``` - -## Extending UserModel +$user = $users->getById($userId); -It is possible extending the UserModel table, since you create a new class extending from UserModel to add the new fields. +// Set a property (update or create) +$user->set('phone', '555-1234'); -For example, imagine your table has one field called "otherfield". +// Save changes +$users->save($user); +``` -You'll have to extend like this: +## Logout ```php setOtherfield($field); - } - - public function getOtherfield() - { - return $this->otherfield; - } - - public function setOtherfield($otherfield) - { - $this->otherfield = $otherfield; - } -} +$sessionContext->registerLogout(); ``` -After that you can use your new definition: +## JWT Token Authentication + +For stateless API authentication, you can use JWT tokens: ```php createAuthToken( + 'johndoe', // Login + 'SecurePass123', // Password + $jwtWrapper, + 3600, // Expires in 1 hour (seconds) + [], // Additional user info to save + ['role' => 'admin'] // Additional token data ); + +// Validate token +$result = $users->isValidToken('johndoe', $jwtWrapper, $token); +if ($result !== null) { + $user = $result['user']; + $tokenData = $result['data']; +} ``` -## Install +See [JWT Tokens](docs/jwt-tokens.md) for detailed information. -Just type: +## Database Schema -```bash -composer require "byjg/authuser" +The default database schema uses two tables: + +```sql +CREATE TABLE users ( + userid INTEGER AUTO_INCREMENT NOT NULL, + name VARCHAR(50), + email VARCHAR(120), + username VARCHAR(15) NOT NULL, + password CHAR(40) NOT NULL, + created DATETIME, + admin ENUM('Y','N'), + CONSTRAINT pk_users PRIMARY KEY (userid) +) ENGINE=InnoDB; + +CREATE TABLE users_property ( + customid INTEGER AUTO_INCREMENT NOT NULL, + name VARCHAR(20), + value VARCHAR(100), + userid INTEGER NOT NULL, + CONSTRAINT pk_custom PRIMARY KEY (customid), + CONSTRAINT fk_custom_user FOREIGN KEY (userid) REFERENCES users (userid) +) ENGINE=InnoDB; ``` +You can customize table and column names using `UserDefinition` and `UserPropertiesDefinition`. See [Database Storage](docs/database-storage.md) for details. + +## Features + +- **Complete User Management** - Create, read, update, and delete users +- **Flexible Authentication** - Username/email + password or JWT tokens +- **Session Management** - PSR-6 compatible cache storage +- **User Properties** - Store custom key-value data per user +- **Password Validation** - Built-in password strength requirements +- **Multiple Storage Backends** - Database (MySQL, PostgreSQL, SQLite, etc.) or XML files +- **Customizable Schema** - Map to existing database tables +- **Field Mappers** - Transform data during read/write operations +- **Extensible User Model** - Add custom fields easily + ## Running Tests Because this project uses PHP Session you need to run the unit test the following manner: @@ -305,15 +266,41 @@ Because this project uses PHP Session you need to run the unit test the followin ./vendor/bin/phpunit --stderr ``` +## Architecture + +```text + ┌───────────────────┐ + │ SessionContext │ + └───────────────────┘ + │ +┌────────────────────────┐ ┌────────────────────────┐ +│ UserDefinition │─ ─ ┐ │ ─ ─ ┤ UserModel │ +└────────────────────────┘ ┌───────────────────┐ │ └────────────────────────┘ +┌────────────────────────┐ └────│ UsersInterface │────┐ ┌────────────────────────┐ +│ UserPropertyDefinition │─ ─ ┘ └───────────────────┘ ─ ─ ┤ UserPropertyModel │ +└────────────────────────┘ ▲ └────────────────────────┘ + │ + ┌────────────────────────┼─────────────────────────┐ + │ │ │ + │ │ │ + │ │ │ + ┌───────────────────┐ ┌───────────────────┐ ┌────────────────────┐ + │ UsersAnyDataset │ │ UsersDBDataset │ │ Custom Impl. │ + └───────────────────┘ └───────────────────┘ └────────────────────┘ +``` + ## Dependencies -```mermaid -flowchart TD +```mermaid +flowchart TD byjg/authuser --> byjg/micro-orm byjg/authuser --> byjg/cache-engine - byjg/authuser --> byjg/jwt-wrapper + byjg/authuser --> byjg/jwt-wrapper ``` +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. ---- [Open source ByJG](http://opensource.byjg.com) diff --git a/docs/authentication.md b/docs/authentication.md new file mode 100644 index 0000000..b18cc31 --- /dev/null +++ b/docs/authentication.md @@ -0,0 +1,150 @@ +--- +sidebar_position: 4 +title: Authentication +--- + +# Authentication + +## Validating User Credentials + +Use the `isValidUser()` method to validate a username/email and password combination: + +```php +isValidUser('johndoe', 'SecurePass123'); + +if ($user !== null) { + echo "Authentication successful!"; + echo "User ID: " . $user->getUserid(); +} else { + echo "Invalid credentials"; +} +``` + +:::tip Login Field +The `isValidUser()` method uses the login field defined in your `UserDefinition`. This can be either the email or username field. +::: + +## Password Hashing + +By default, passwords are automatically hashed using SHA-1 when saved. The library uses the `PasswordSha1Mapper` for this purpose. + +```php +setPassword('plaintext password'); +$users->save($user); + +// The password is stored as SHA-1 hash in the database +``` + +:::warning SHA-1 Deprecation +SHA-1 is used for backward compatibility. For new projects, consider implementing a custom password hasher using bcrypt or Argon2. See [Custom Mappers](mappers.md) for details. +::: + +## Workflow + +### Basic Authentication Flow + +```php +isValidUser('johndoe', 'SecurePass123'); + +if ($user !== null) { + // 2. Create session context + $sessionContext = new SessionContext(Factory::createSessionPool()); + + // 3. Register login + $sessionContext->registerLogin($user->getUserid()); + + // 4. User is now authenticated + echo "Welcome, " . $user->getName(); +} +``` + +### Checking Authentication Status + +```php +isAuthenticated()) { + $userId = $sessionContext->userInfo(); + $user = $users->getById($userId); + echo "Hello, " . $user->getName(); +} else { + echo "Please log in"; +} +``` + +### Logging Out + +```php +registerLogout(); +``` + +## JWT Token Authentication + +For stateless authentication, you can use JWT tokens: + +```php +createAuthToken( + 'johndoe', // Login + 'SecurePass123', // Password + $jwtWrapper, + 3600, // Expires in 1 hour (seconds) + [], // Additional user info to save + ['role' => 'admin'] // Additional token data +); + +if ($token !== null) { + echo "Token: " . $token; +} +``` + +### Validating JWT Tokens + +```php +isValidToken('johndoe', $jwtWrapper, $token); + +if ($result !== null) { + $user = $result['user']; + $tokenData = $result['data']; + + echo "User: " . $user->getName(); + echo "Role: " . $tokenData['role']; +} +``` + +:::info Token Storage +When a JWT token is created, a hash of the token is stored in the user's properties as `TOKEN_HASH`. This ensures tokens can be invalidated if needed. +::: + +## Security Best Practices + +1. **Always use HTTPS** in production to prevent credential theft +2. **Implement rate limiting** to prevent brute force attacks +3. **Use strong passwords** - see [Password Validation](password-validation.md) +4. **Set appropriate session timeouts** +5. **Validate and sanitize** all user inputs + +## Next Steps + +- [Session Context](session-context.md) - Manage user sessions +- [JWT Tokens](jwt-tokens.md) - Deep dive into JWT authentication +- [Password Validation](password-validation.md) - Enforce password policies diff --git a/docs/custom-fields.md b/docs/custom-fields.md new file mode 100644 index 0000000..04d2bc5 --- /dev/null +++ b/docs/custom-fields.md @@ -0,0 +1,375 @@ +--- +sidebar_position: 10 +title: Custom Fields +--- + +# Custom Fields + +You can extend the `UserModel` to add custom fields that match your database schema. + +## Extending UserModel + +### Creating a Custom User Model + +```php +phone = $phone; + $this->department = $department; + } + + public function getPhone(): ?string + { + return $this->phone; + } + + public function setPhone(?string $phone): void + { + $this->phone = $phone; + } + + public function getDepartment(): ?string + { + return $this->department; + } + + public function setDepartment(?string $department): void + { + $this->department = $department; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(?string $title): void + { + $this->title = $title; + } + + public function getProfilePicture(): ?string + { + return $this->profilePicture; + } + + public function setProfilePicture(?string $profilePicture): void + { + $this->profilePicture = $profilePicture; + } +} +``` + +## Database Schema + +Add the custom fields to your users table: + +```sql +CREATE TABLE users +( + userid INTEGER AUTO_INCREMENT NOT NULL, + name VARCHAR(50), + email VARCHAR(120), + username VARCHAR(15) NOT NULL, + password CHAR(40) NOT NULL, + created DATETIME, + admin ENUM('Y','N'), + -- Custom fields + phone VARCHAR(20), + department VARCHAR(50), + title VARCHAR(50), + profile_picture VARCHAR(255), + + CONSTRAINT pk_users PRIMARY KEY (userid) +) ENGINE=InnoDB; +``` + +## Configuring UserDefinition + +Map the custom fields in your `UserDefinition`: + +```php + 'userid', + UserDefinition::FIELD_NAME => 'name', + UserDefinition::FIELD_EMAIL => 'email', + UserDefinition::FIELD_USERNAME => 'username', + UserDefinition::FIELD_PASSWORD => 'password', + UserDefinition::FIELD_CREATED => 'created', + UserDefinition::FIELD_ADMIN => 'admin', + // Custom fields + 'phone' => 'phone', + 'department' => 'department', + 'title' => 'title', + 'profilePicture' => 'profile_picture' + ] +); +``` + +## Using the Custom Model + +### Creating Users + +```php +setName('John Doe'); +$user->setEmail('john@example.com'); +$user->setUsername('johndoe'); +$user->setPassword('SecurePass123'); +$user->setPhone('+1-555-1234'); +$user->setDepartment('Engineering'); +$user->setTitle('Senior Developer'); + +$users->save($user); +``` + +### Retrieving Users + +```php +getById($userId); + +// Access custom fields +echo $user->getName(); +echo $user->getPhone(); +echo $user->getDepartment(); +echo $user->getTitle(); +``` + +### Updating Custom Fields + +```php +getById($userId); +$user->setDepartment('Sales'); +$user->setTitle('Sales Manager'); +$users->save($user); +``` + +## Read-Only Fields + +You can mark fields as read-only to prevent updates: + +```php +markPropertyAsReadOnly(UserDefinition::FIELD_CREATED); + +// Make custom field read-only +$userDefinition->markPropertyAsReadOnly('phone'); +``` + +Read-only fields: +- Can be set during creation +- Cannot be updated after creation +- Are ignored during updates + +## Auto-Generated Fields + +### Auto-Increment IDs + +For auto-increment IDs, the database handles generation automatically. No configuration needed. + +### UUID Fields + +For UUID primary keys: + +```php +defineGenerateKey(UserIdGeneratorMapper::class); +``` + +### Custom ID Generation + +Create a custom mapper for custom ID generation: + +```php +defineGenerateKey(CustomIdMapper::class); +``` + +## Field Transformation + +You can transform fields during read/write operations using mappers. See [Mappers](mappers.md) for details. + +## Complex Data Types + +### JSON Fields + +For storing JSON data in custom fields: + +```php +defineMapperForUpdate('metadata', JsonMapper::class); +$userDefinition->defineMapperForSelect('metadata', JsonDecodeMapper::class); +``` + +### Date/Time Fields + +```php +format('Y-m-d H:i:s'); + } + return $value; + } +} + +$userDefinition->defineMapperForUpdate('created', DateTimeMapper::class); +``` + +## Complete Example + +```php + 'userid', + UserDefinition::FIELD_NAME => 'name', + UserDefinition::FIELD_EMAIL => 'email', + UserDefinition::FIELD_USERNAME => 'username', + UserDefinition::FIELD_PASSWORD => 'password', + UserDefinition::FIELD_CREATED => 'created', + UserDefinition::FIELD_ADMIN => 'admin', + 'phone' => 'phone', + 'department' => 'department', + 'title' => 'title', + 'profilePicture' => 'profile_picture' + ] +); + +// Make created field read-only +$userDefinition->markPropertyAsReadOnly(UserDefinition::FIELD_CREATED); + +// Initialize user management +$users = new UsersDBDataset($dbDriver, $userDefinition); + +// Create a user +$user = new CustomUserModel(); +$user->setName('Jane Smith'); +$user->setEmail('jane@example.com'); +$user->setUsername('janesmith'); +$user->setPassword('SecurePass123'); +$user->setPhone('+1-555-5678'); +$user->setDepartment('Marketing'); +$user->setTitle('Marketing Director'); + +$savedUser = $users->save($user); + +// Retrieve and update +$user = $users->getById($savedUser->getUserid()); +$user->setTitle('VP of Marketing'); +$users->save($user); +``` + +## When to Use Custom Fields vs Properties + +| Use Custom Fields When | Use Properties When | +|------------------------|---------------------| +| Field is used frequently | Field is rarely used | +| Field is searched/filtered | Field is key-value metadata | +| Field is fixed schema | Field is dynamic/flexible | +| Better performance needed | Schema flexibility needed | +| Field is required | Field is optional | + +## Next Steps + +- [Mappers](mappers.md) - Custom field transformations +- [Database Storage](database-storage.md) - Schema configuration +- [User Properties](user-properties.md) - Flexible metadata storage diff --git a/docs/database-storage.md b/docs/database-storage.md new file mode 100644 index 0000000..8e9c872 --- /dev/null +++ b/docs/database-storage.md @@ -0,0 +1,256 @@ +--- +sidebar_position: 7 +title: Database Storage +--- + +# Database Storage + +The library supports storing users in relational databases through the `UsersDBDataset` class. + +## Database Setup + +### Default Schema + +The default database structure uses two tables: + +```sql +CREATE TABLE users +( + userid INTEGER AUTO_INCREMENT NOT NULL, + name VARCHAR(50), + email VARCHAR(120), + username VARCHAR(15) NOT NULL, + password CHAR(40) NOT NULL, + created DATETIME, + admin ENUM('Y','N'), + + CONSTRAINT pk_users PRIMARY KEY (userid) +) ENGINE=InnoDB; + +CREATE TABLE users_property +( + customid INTEGER AUTO_INCREMENT NOT NULL, + name VARCHAR(20), + value VARCHAR(100), + userid INTEGER NOT NULL, + + CONSTRAINT pk_custom PRIMARY KEY (customid), + CONSTRAINT fk_custom_user FOREIGN KEY (userid) REFERENCES users (userid) +) ENGINE=InnoDB; +``` + +## Basic Usage + +### Using Default Configuration + +```php + 'user_id', + UserDefinition::FIELD_NAME => 'full_name', + UserDefinition::FIELD_EMAIL => 'email_address', + UserDefinition::FIELD_USERNAME => 'user_name', + UserDefinition::FIELD_PASSWORD => 'password_hash', + UserDefinition::FIELD_CREATED => 'date_created', + UserDefinition::FIELD_ADMIN => 'is_admin' + ] +); + +$users = new UsersDBDataset($dbDriver, $userDefinition); +``` + +### Custom Properties Table + +```php + 'id', + UserDefinition::FIELD_NAME => 'fullname', + UserDefinition::FIELD_EMAIL => 'email', + UserDefinition::FIELD_USERNAME => 'username', + UserDefinition::FIELD_PASSWORD => 'pwd', + UserDefinition::FIELD_CREATED => 'created_at', + UserDefinition::FIELD_ADMIN => 'is_admin' + ] +); + +// Custom properties definition +$propertiesDefinition = new UserPropertiesDefinition( + 'app_user_meta', + 'id', + 'meta_key', + 'meta_value', + 'user_id' +); + +// Initialize +$users = new UsersDBDataset($dbDriver, $userDefinition, $propertiesDefinition); + +// Use it +$user = $users->addUser('John Doe', 'johndoe', 'john@example.com', 'password123'); +``` + +## XML/File Storage + +For simple applications or development, you can use XML file storage: + +```php +addUser('John Doe', 'johndoe', 'john@example.com', 'password123'); +``` + +:::warning Production Use +XML file storage is suitable for development and small applications. For production applications with multiple users, use database storage. +::: + +## Architecture + +```text + ┌───────────────────┐ + │ SessionContext │ + └───────────────────┘ + │ +┌────────────────────────┐ ┌────────────────────────┐ +│ UserDefinition │─ ─ ┐ │ ─ ─ ┤ UserModel │ +└────────────────────────┘ ┌───────────────────┐ │ └────────────────────────┘ +┌────────────────────────┐ └────│ UsersInterface │────┐ ┌────────────────────────┐ +│ UserPropertyDefinition │─ ─ ┘ └───────────────────┘ ─ ─ ┤ UserPropertyModel │ +└────────────────────────┘ ▲ └────────────────────────┘ + │ + ┌────────────────────────┼─────────────────────────┐ + │ │ │ + │ │ │ + │ │ │ + ┌───────────────────┐ ┌───────────────────┐ ┌────────────────────┐ + │ UsersAnyDataset │ │ UsersDBDataset │ │ Custom Impl. │ + └───────────────────┘ └───────────────────┘ └────────────────────┘ +``` + +- **UserInterface**: Base interface for all implementations +- **UsersDBDataset**: Database implementation +- **UsersAnyDataset**: XML file implementation +- **UserModel**: The user data model +- **UserPropertyModel**: The user property data model +- **UserDefinition**: Maps model to database schema +- **UserPropertiesDefinition**: Maps properties to database schema + +## Next Steps + +- [User Management](user-management.md) - Managing users +- [Custom Fields](custom-fields.md) - Extending UserModel +- [Mappers](mappers.md) - Custom field transformations diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 0000000..deba131 --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,520 @@ +--- +sidebar_position: 12 +title: Complete Examples +--- + +# Complete Examples + +This page contains complete, working examples for common use cases. + +## Simple Web Application + +### Setup + +```php +isValidUser($username, $password); + + if ($user !== null) { + $sessionContext->registerLogin($user->getUserid()); + $sessionContext->setSessionData('login_time', time()); + + header('Location: dashboard.php'); + exit; + } else { + $error = 'Invalid username or password'; + } + } catch (Exception $e) { + $error = 'An error occurred: ' . $e->getMessage(); + } +} +?> + + + + Login + + +

Login

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

Create an account

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

Create Account

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

Already have an account?

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

Welcome, getName()) ?>

+ +

Email: getEmail()) ?>

+

Logged in at:

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

You are an administrator

+

Admin Panel

+ + +

Edit Profile

+

Logout

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

Email: getEmail()) ?>

Logged in at:

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

You are an administrator

Admin Panel

@@ -364,7 +364,7 @@ try { 'name' => $user->getName(), 'email' => $user->getEmail(), 'username' => $user->getUsername(), - 'admin' => $users->isAdmin($user->getUserid()) + 'admin' => $user->isAdmin() ]); } elseif ($_SERVER['REQUEST_METHOD'] === 'PUT') { // Update user info diff --git a/docs/user-management.md b/docs/user-management.md index 3ed82a4..5a4456c 100644 --- a/docs/user-management.md +++ b/docs/user-management.md @@ -122,7 +122,8 @@ $users->removeByLoginField('johndoe'); ```php isAdmin($userId)) { +/** @var $user \ByJG\Authenticate\Model\UserModel */ +if ($user->isAdmin()) { echo "User is an administrator"; } ``` diff --git a/src/Interfaces/UsersInterface.php b/src/Interfaces/UsersInterface.php index 82f12cc..aa52622 100644 --- a/src/Interfaces/UsersInterface.php +++ b/src/Interfaces/UsersInterface.php @@ -91,13 +91,6 @@ public function removeByLoginField(string $login): bool; */ public function isValidUser(string $userName, string $password): UserModel|null; - /** - * - * @param string|int|HexUuidLiteral $userId - * @return bool - */ - public function isAdmin(string|HexUuidLiteral|int $userId): bool; - /** * @desc Check if the user have rights to edit specific site. * @param string|int|HexUuidLiteral|null $userId diff --git a/src/Model/UserModel.php b/src/Model/UserModel.php index 82cf36d..4209cff 100644 --- a/src/Model/UserModel.php +++ b/src/Model/UserModel.php @@ -224,4 +224,11 @@ public function withPasswordDefinition(PasswordDefinition $passwordDefinition): $this->passwordDefinition = $passwordDefinition; return $this; } + + public function isAdmin(): bool + { + return + preg_match('/^(yes|YES|[yY]|true|TRUE|[tT]|1|[sS])$/', $this->getAdmin()) === 1 + ; + } } diff --git a/src/UsersBase.php b/src/UsersBase.php index 80cfe2c..5e08667 100644 --- a/src/UsersBase.php +++ b/src/UsersBase.php @@ -232,13 +232,17 @@ public function isValidUser(string $userName, string $password): UserModel|null public function hasProperty(string|int|HexUuidLiteral|null $userId, string $propertyName, string|null $value = null): bool { //anydataset.Row - $user = $this->getById($userId); + $userIdMapper = $this->getUserDefinition()->getMapperForUpdate(UserDefinition::FIELD_USERID); + if (is_string($userIdMapper)) { + $userIdMapper = new $userIdMapper(); + } + $user = $this->getById($userIdMapper->processedValue($userId, null)); if (empty($user)) { return false; } - if ($this->isAdmin($userId)) { + if ($user->isAdmin()) { return true; } @@ -262,7 +266,6 @@ public function hasProperty(string|int|HexUuidLiteral|null $userId, string $prop * @param string|int|HexUuidLiteral $userId User ID * @param string $propertyName Property name * @return array|string|UserPropertiesModel|null - * @throws UserNotFoundException * @throws InvalidArgumentException */ #[Override] @@ -272,7 +275,7 @@ public function getProperty(string|HexUuidLiteral|int $userId, string $propertyN if ($user !== null) { $values = $user->get($propertyName); - if ($this->isAdmin($userId)) { + if ($user->isAdmin()) { return array(UserDefinition::FIELD_ADMIN => "admin"); } @@ -318,26 +321,6 @@ abstract public function removeProperty(string|HexUuidLiteral|int $userId, strin #[Override] abstract public function removeAllProperties(string $propertyName, string|null $value = null): void; - /** - * @param string|int|HexUuidLiteral $userId - * @return bool - * @throws UserNotFoundException - * @throws InvalidArgumentException - */ - #[Override] - 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 * diff --git a/tests/UsersAnyDatasetByUsernameTest.php b/tests/UsersAnyDatasetByUsernameTest.php index b2452d0..8e336d2 100644 --- a/tests/UsersAnyDatasetByUsernameTest.php +++ b/tests/UsersAnyDatasetByUsernameTest.php @@ -209,7 +209,8 @@ public function testIsValidUser(): void public function testIsAdmin(): void { // Check is Admin - $this->assertFalse($this->object->isAdmin($this->prefix . '3')); + $user3 = $this->object->getById($this->prefix . '3'); + $this->assertFalse($user3->isAdmin()); // Set the Admin Flag $login = $this->__chooseValue('user3', 'user3@gmail.com'); @@ -218,7 +219,8 @@ public function testIsAdmin(): void $this->object->save($user); // Check is Admin - $this->assertTrue($this->object->isAdmin($this->prefix . '3')); + $user3 = $this->object->getById($this->prefix . '3'); + $this->assertTrue($user3->isAdmin()); } protected function expectedToken($tokenData, $login, $userId): void From 6df1a9fa9283b5c2e54be74f20081d8567a4a6f6 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Tue, 11 Nov 2025 19:56:09 -0500 Subject: [PATCH 17/40] Refactor user retrieval methods: consolidate `getById`, `getByEmail`, `getByUsername`, and `getByLoginField` into a single `get` method, update all references, adjust tests, and update documentation accordingly. --- docs/authentication.md | 2 +- docs/custom-fields.md | 6 +- docs/database-storage.md | 2 +- docs/examples.md | 2 +- docs/jwt-tokens.md | 2 +- docs/password-validation.md | 2 +- docs/session-context.md | 2 +- docs/user-management.md | 30 ++++++++-- docs/user-properties.md | 4 +- example.php | 6 +- src/Interfaces/UsersInterface.php | 26 +-------- src/UsersAnyDataset.php | 6 +- src/UsersBase.php | 74 +++++++------------------ src/UsersDBDataset.php | 31 +++++++++-- tests/UsersAnyDatasetByUsernameTest.php | 59 ++++++++++---------- tests/UsersDBDatasetByUsernameTest.php | 12 ++-- tests/UsersDBDatasetDefinitionTest.php | 8 +-- 17 files changed, 132 insertions(+), 142 deletions(-) diff --git a/docs/authentication.md b/docs/authentication.md index 7ba0842..8a52ed4 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -133,7 +133,7 @@ $sessionContext = new SessionContext(Factory::createSessionPool()); if ($sessionContext->isAuthenticated()) { $userId = $sessionContext->userInfo(); - $user = $users->getById($userId); + $user = $users->get($userId); echo "Hello, " . $user->getName(); } else { echo "Please log in"; diff --git a/docs/custom-fields.md b/docs/custom-fields.md index 050c6e8..4312bb6 100644 --- a/docs/custom-fields.md +++ b/docs/custom-fields.md @@ -166,7 +166,7 @@ $users->save($user); ```php getById($userId); +$user = $users->get($userId); // Access custom fields echo $user->getName(); @@ -179,7 +179,7 @@ echo $user->getTitle(); ```php getById($userId); +$user = $users->get($userId); $user->setDepartment('Sales'); $user->setTitle('Sales Manager'); $users->save($user); @@ -357,7 +357,7 @@ $user->setTitle('Marketing Director'); $savedUser = $users->save($user); // Retrieve and update -$user = $users->getById($savedUser->getUserid()); +$user = $users->get($savedUser->getUserid()); $user->setTitle('VP of Marketing'); $users->save($user); ``` diff --git a/docs/database-storage.md b/docs/database-storage.md index 8e9c872..4484591 100644 --- a/docs/database-storage.md +++ b/docs/database-storage.md @@ -148,7 +148,7 @@ $userDefinition = new UserDefinition( ``` :::tip Login Field -The login field affects methods like `isValidUser()` and `getByLoginField()`. They will use the configured field for authentication. +The login field affects methods like `isValidUser()`. They will use the configured field for authentication. ::: ## Complete Example diff --git a/docs/examples.md b/docs/examples.md index 184c36e..0d04a97 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -203,7 +203,7 @@ if (!$sessionContext->isAuthenticated()) { // Get current user $userId = $sessionContext->userInfo(); -$user = $users->getById($userId); +$user = $users->get($userId); $loginTime = $sessionContext->getSessionData('login_time'); ?> diff --git a/docs/jwt-tokens.md b/docs/jwt-tokens.md index 0993d28..02cdf2d 100644 --- a/docs/jwt-tokens.md +++ b/docs/jwt-tokens.md @@ -285,7 +285,7 @@ if (preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) { $jwtData = $jwtWrapper->extractData($token); $username = $jwtData->data['login'] ?? null; - $user = $users->getByLoginField($username); + $user = $users->get($username, $users->getUserDefinition()->loginField()); if ($user !== null) { $users->removeProperty($user->getUserid(), 'TOKEN_HASH'); } diff --git a/docs/password-validation.md b/docs/password-validation.md index 8df36b7..1306ac4 100644 --- a/docs/password-validation.md +++ b/docs/password-validation.md @@ -253,7 +253,7 @@ Repeated patterns include: getById($userId); + $user = $users->get($userId); $user->withPasswordDefinition($passwordDefinition); // Verify old password diff --git a/docs/session-context.md b/docs/session-context.md index d0f9290..d361105 100644 --- a/docs/session-context.md +++ b/docs/session-context.md @@ -169,7 +169,7 @@ if (!$sessionContext->isAuthenticated()) { } $userId = $sessionContext->userInfo(); -$user = $users->getById($userId); +$user = $users->get($userId); $loginTime = $sessionContext->getSessionData('login_time'); echo "Welcome, " . $user->getName(); diff --git a/docs/user-management.md b/docs/user-management.md index 5a4456c..cd749c6 100644 --- a/docs/user-management.md +++ b/docs/user-management.md @@ -41,34 +41,48 @@ $savedUser = $users->save($userModel); ## Retrieving Users +To retrieve users you can use the `get($value, ?string $field = null)` method. +When the `$field` argument is omitted it defaults to the primary key +defined in your `UserDefinition`. Passing any other column automatically builds the right filter and throws an +`\InvalidArgumentException` if the field is not one of the allowed values (`userid`, `username`, or `email`). + +```php +get('john@example.com', $users->getUserDefinition()->getEmail()); +``` + +The following examples show the common calls: + ### Get User by ID ```php getById($userId); +$user = $users->get($userId); +# OR +$user = $users->get($userId, $users->getUserDefinition()->getUserid()); ``` ### Get User by Email ```php getByEmail('john@example.com'); +$user = $users->get('john@example.com', $users->getUserDefinition()->getEmail()); ``` ### Get User by Username ```php getByUsername('johndoe'); +$user = $users->get('johndoe', $users->getUserDefinition()->getUsername()); ``` ### Get User by Login Field -The login field is determined by the `UserDefinition` (either email or username): +The login field is determined by the `UserDefinition::loginField()` (either email or username): ```php getByLoginField('johndoe'); +$user = $users->get('johndoe', $users->getUserDefinition()->loginField()); ``` ### Using Custom Filters @@ -92,7 +106,7 @@ $user = $users->getUser($filter); ```php getById($userId); +$user = $users->get($userId); // Update fields $user->setName('Jane Doe'); @@ -120,6 +134,10 @@ $users->removeByLoginField('johndoe'); ## Checking Admin Status +The admin flag is now interpreted entirely inside `UserModel`. Use `$user->isAdmin()` to read the computed boolean value, +and `$user->setAdmin(true)` (or one of the accepted string values) to change it. This replaces the old `$users->isAdmin()` +method that lived in `UsersInterface`. + ```php getById($userId); +$user = $users->get($userId); // Set a property value $user->set('phone', '555-1234'); @@ -88,7 +88,7 @@ Returns `null` if the property doesn't exist. ```php getById($userId); +$user = $users->get($userId); // Get property value(s) $phone = $user->get('phone'); diff --git a/example.php b/example.php index 49c2da6..a33cf2f 100644 --- a/example.php +++ b/example.php @@ -2,9 +2,9 @@ require "vendor/autoload.php"; -use ByJG\Authenticate\UsersAnyDataset; -use ByJG\Authenticate\SessionContext; use ByJG\AnyDataset\Core\AnyDataset; +use ByJG\Authenticate\SessionContext; +use ByJG\Authenticate\UsersAnyDataset; use ByJG\Cache\Factory; // Create or load AnyDataset from XML file @@ -35,7 +35,7 @@ $session->setSessionData('login_time', time()); // Get the user info - $currentUser = $users->getById($session->userInfo()); + $currentUser = $users->get($session->userInfo()); echo "Welcome, " . $currentUser->getName() . "\n"; } diff --git a/src/Interfaces/UsersInterface.php b/src/Interfaces/UsersInterface.php index aa52622..2facd33 100644 --- a/src/Interfaces/UsersInterface.php +++ b/src/Interfaces/UsersInterface.php @@ -50,31 +50,11 @@ public function getUser(IteratorFilter $filter): UserModel|null; /** * Enter description here... * - * @param string|HexUuidLiteral|int $userid + * @param string|HexUuidLiteral|int $value + * @param string|null $field * @return UserModel|null */ - public function getById(string|HexUuidLiteral|int $userid): UserModel|null; - - /** - * @desc Get the user based on his email. - * @param string $email Email to find - * @return UserModel|null if user was found; null, otherwise - */ - public function getByEmail(string $email): UserModel|null; - - /** - * @desc Get the user based on his username. - * @param string $username - * @return UserModel|null if user was found; null, otherwise - */ - public function getByUsername(string $username): UserModel|null; - - /** - * @desc Get the user based on his login - * @param string $login - * @return UserModel|null if user was found; null, otherwise - */ - public function getByLoginField(string $login): UserModel|null; + public function get(string|HexUuidLiteral|int $value, ?string $field = null): UserModel|null; /** * @desc Remove the user based on his login. diff --git a/src/UsersAnyDataset.php b/src/UsersAnyDataset.php index e184362..3f7b489 100644 --- a/src/UsersAnyDataset.php +++ b/src/UsersAnyDataset.php @@ -211,7 +211,7 @@ public function getUsersByPropertySet(array $propertiesArray): array public function addProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value): bool { //anydataset.Row - $user = $this->getById($userId); + $user = $this->get($userId); if ($user !== null) { if (!$this->hasProperty($user->getUserid(), $propertyName, $value)) { $user->addProperty(new UserPropertiesModel($propertyName, $value)); @@ -232,7 +232,7 @@ public function addProperty(string|HexUuidLiteral|int $userId, string $propertyN #[Override] public function setProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value): bool { - $user = $this->getById($userId); + $user = $this->get($userId); if ($user !== null) { $user->set($propertyName, $value); $this->save($user); @@ -254,7 +254,7 @@ public function setProperty(string|HexUuidLiteral|int $userId, string $propertyN #[Override] public function removeProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value = null): bool { - $user = $this->getById($userId); + $user = $this->get($userId); if (!empty($user)) { $properties = $user->getProperties(); foreach ($properties as $key => $property) { diff --git a/src/UsersBase.php b/src/UsersBase.php index 5e08667..706f068 100644 --- a/src/UsersBase.php +++ b/src/UsersBase.php @@ -96,7 +96,7 @@ public function addUser(string $name, string $userName, string $email, string $p #[Override] public function canAddUser(UserModel $model): bool { - if ($this->getByEmail($model->getEmail()) !== null) { + if (!empty($model->getUserid()) && $this->get($model->getUserid()) !== null) { throw new UserExistsException('Email already exists'); } $filter = new IteratorFilter(); @@ -119,66 +119,32 @@ public function canAddUser(UserModel $model): bool 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 - */ - #[Override] - 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. + * Get the user based on his id. * Return Row if user was found; null, otherwise * - * @param string $username + * @param string|HexUuidLiteral|int $value + * @param string|null $field * @return UserModel|null * @throws InvalidArgumentException */ #[Override] - public function getByUsername(string $username): UserModel|null + public function get(string|HexUuidLiteral|int $value, ?string $field = null): 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 - */ - #[Override] - public function getByLoginField(string $login): UserModel|null - { - $filter = new IteratorFilter(); - $filter->and($this->getUserDefinition()->loginField(), Relation::EQUAL, strtolower($login)); + if (empty($field)) { + $field = $this->getUserDefinition()->getUserid(); + } - return $this->getUser($filter); - } + $validFields = [ + $this->getUserDefinition()->getUserid(), + $this->getUserDefinition()->getUsername(), + $this->getUserDefinition()->getEmail() + ]; - /** - * 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 - */ - #[Override] - public function getById(string|HexUuidLiteral|int $userid): UserModel|null - { + if (!in_array($field, $validFields)) { + throw new \InvalidArgumentException("Invalid field type provided. Should be one of " . implode(", ", $validFields)); + } $filter = new IteratorFilter(); - $filter->and($this->getUserDefinition()->getUserid(), Relation::EQUAL, $userid); + $filter->and($field, Relation::EQUAL, $value); return $this->getUser($filter); } @@ -236,7 +202,7 @@ public function hasProperty(string|int|HexUuidLiteral|null $userId, string $prop if (is_string($userIdMapper)) { $userIdMapper = new $userIdMapper(); } - $user = $this->getById($userIdMapper->processedValue($userId, null)); + $user = $this->get($userIdMapper->processedValue($userId, null)); if (empty($user)) { return false; @@ -271,7 +237,7 @@ public function hasProperty(string|int|HexUuidLiteral|null $userId, string $prop #[Override] public function getProperty(string|HexUuidLiteral|int $userId, string $propertyName): array|string|UserPropertiesModel|null { - $user = $this->getById($userId); + $user = $this->get($userId); if ($user !== null) { $values = $user->get($propertyName); @@ -382,7 +348,7 @@ public function createAuthToken( #[Override] public function isValidToken(string $login, JwtWrapper $jwtWrapper, string $token): array|null { - $user = $this->getByLoginField($login); + $user = $this->get($login, $this->getUserDefinition()->loginField()); if (is_null($user)) { throw new UserNotFoundException('User not found!'); diff --git a/src/UsersDBDataset.php b/src/UsersDBDataset.php index f3cce7b..4c04845 100644 --- a/src/UsersDBDataset.php +++ b/src/UsersDBDataset.php @@ -152,7 +152,7 @@ public function save(UserModel $model): UserModel } if ($newUser) { - $model = $this->getById($model->getUserid()); + $model = $this->get($model->getUserid()); } if ($model === null) { @@ -162,6 +162,29 @@ public function save(UserModel $model): UserModel return $model; } + #[\Override] + public function get(string|HexUuidLiteral|int $value, ?string $field = null): ?UserModel + { + if (empty($field)) { + $field = $this->getUserDefinition()->getUserid(); + } + + $function = match ($field) { + $this->getUserDefinition()->getEmail() => $this->getUserDefinition()->getMapperForUpdate(UserDefinition::FIELD_EMAIL), + $this->getUserDefinition()->getUsername() => $this->getUserDefinition()->getMapperForUpdate(UserDefinition::FIELD_USERNAME), + default => $this->getUserDefinition()->getMapperForUpdate(UserDefinition::FIELD_USERID), + }; + + if (!empty($function)) { + if (is_string($function)) { + $function = new $function(); + } + $value = $function->processedValue($value, null); + } + + return parent::get($value, $field); + } + /** * Get the users database information based on a filter. * @@ -228,7 +251,7 @@ public function getUser(IteratorFilter $filter): UserModel|null #[Override] public function removeByLoginField(string $login): bool { - $user = $this->getByLoginField($login); + $user = $this->get($login, $this->getUserDefinition()->loginField()); if ($user !== null) { return $this->removeUserById($user->getUserid()); @@ -327,7 +350,7 @@ public function getUsersByPropertySet(array $propertiesArray): array public function addProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value): bool { //anydataset.Row - $user = $this->getById($userId); + $user = $this->get($userId); if (empty($user)) { return false; } @@ -396,7 +419,7 @@ public function setProperty(string|HexUuidLiteral|int $userId, string $propertyN #[Override] public function removeProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value = null): bool { - $user = $this->getById($userId); + $user = $this->get($userId); if ($user !== null) { $updateable = DeleteQuery::getInstance() diff --git a/tests/UsersAnyDatasetByUsernameTest.php b/tests/UsersAnyDatasetByUsernameTest.php index 8e336d2..e258900 100644 --- a/tests/UsersAnyDatasetByUsernameTest.php +++ b/tests/UsersAnyDatasetByUsernameTest.php @@ -52,7 +52,7 @@ public function __setUp($loginField) $this->object->addUser('User 3', 'user3', 'user3@gmail.com', 'pwd3'); } - public function __chooseValue($forUsername, $forEmail) + public function __chooseValue($forUsername, $forEmail): string { $searchForList = [ $this->userDefinition->getUsername() => $forUsername, @@ -74,7 +74,10 @@ public function testAddUser() { $this->object->addUser('John Doe', 'john', 'johndoe@gmail.com', 'mypassword'); - $user = $this->object->getByLoginField($this->__chooseValue('john', 'johndoe@gmail.com')); + $user = $this->object->get( + $this->__chooseValue('john', 'johndoe@gmail.com'), + $this->object->getUserDefinition()->loginField() + ); $this->assertEquals('john', $user->getUserid()); $this->assertEquals('John Doe', $user->getName()); @@ -92,17 +95,17 @@ public function testAddUserError(): void public function testAddProperty(): void { // Check state - $user = $this->object->getById($this->prefix . '2'); + $user = $this->object->get($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'); + $user = $this->object->get($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'); + $user = $this->object->get($this->prefix . '2'); $this->assertEquals(['Rio de Janeiro', 'Belo Horizonte'], $user->get('city')); // Get Property @@ -110,12 +113,12 @@ public function testAddProperty(): void // Add another property $this->object->addProperty($this->prefix . '2', 'state', 'RJ'); - $user = $this->object->getById($this->prefix . '2'); + $user = $this->object->get($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'); + $user = $this->object->get($this->prefix . '2'); $this->assertEmpty($user->get('state')); // Remove Property Again @@ -130,32 +133,32 @@ public function testRemoveAllProperties(): void $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'); + $user = $this->object->get($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'); + $user = $this->object->get($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'); + $user = $this->object->get($this->prefix . '2'); $this->assertEquals(['Rio de Janeiro', 'Niteroi'], $user->get('city')); $this->assertEmpty($user->get('state')); - $user = $this->object->getById($this->prefix . '1'); + $user = $this->object->get($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'); + $user = $this->object->get($this->prefix . '2'); $this->assertEquals('Rio de Janeiro', $user->get('city')); $this->assertEmpty($user->get('state')); - $user = $this->object->getById($this->prefix . '1'); + $user = $this->object->get($this->prefix . '1'); $this->assertEmpty($user->get('city')); $this->assertEmpty($user->get('state')); @@ -165,13 +168,13 @@ public function testRemoveByLoginField(): void { $login = $this->__chooseValue('user1', 'user1@gmail.com'); - $user = $this->object->getByLoginField($login); + $user = $this->object->get($login, $this->object->getUserDefinition()->loginField()); $this->assertNotNull($user); $result = $this->object->removeByLoginField($login); $this->assertTrue($result); - $user = $this->object->getByLoginField($login); + $user = $this->object->get($login, $this->object->getUserDefinition()->loginField()); $this->assertNull($user); } @@ -180,7 +183,7 @@ public function testEditUser(): void $login = $this->__chooseValue('user1', 'user1@gmail.com'); // Getting data - $user = $this->object->getByLoginField($login); + $user = $this->object->get($login, $this->object->getUserDefinition()->loginField()); $this->assertEquals('User 1', $user->getName()); // Change and Persist data @@ -188,7 +191,7 @@ public function testEditUser(): void $this->object->save($user); // Check if data persists - $user = $this->object->getById($this->prefix . '1'); + $user = $this->object->get($this->prefix . '1'); $this->assertEquals('Other name', $user->getName()); } @@ -209,17 +212,17 @@ public function testIsValidUser(): void public function testIsAdmin(): void { // Check is Admin - $user3 = $this->object->getById($this->prefix . '3'); + $user3 = $this->object->get($this->prefix . '3'); $this->assertFalse($user3->isAdmin()); // Set the Admin Flag $login = $this->__chooseValue('user3', 'user3@gmail.com'); - $user = $this->object->getByLoginField($login); + $user = $this->object->get($login, $this->object->getUserDefinition()->loginField()); $user->setAdmin('Y'); $this->object->save($user); // Check is Admin - $user3 = $this->object->getById($this->prefix . '3'); + $user3 = $this->object->get($this->prefix . '3'); $this->assertTrue($user3->isAdmin()); } @@ -238,7 +241,7 @@ protected function expectedToken($tokenData, $login, $userId): void ['tokenData'=>$tokenData] ); - $user = $this->object->getByLoginField($login); + $user = $this->object->get($login, $this->object->getUserDefinition()->loginField()); $dataFromToken = new \stdClass(); $dataFromToken->tokenData = $tokenData; @@ -288,28 +291,28 @@ public function testValidateTokenWithAnotherUser(): void */ public function testSaveAndSave() { - $user = $this->object->getById('user1'); + $user = $this->object->get('user1'); $this->object->save($user); - $user2 = $this->object->getById('user1'); + $user2 = $this->object->get('user1'); $this->assertEquals($user, $user2); } public function testRemoveUserById(): void { - $user = $this->object->getById($this->prefix . '1'); + $user = $this->object->get($this->prefix . '1'); $this->assertNotNull($user); $this->object->removeUserById($this->prefix . '1'); - $user2 = $this->object->getById($this->prefix . '1'); + $user2 = $this->object->get($this->prefix . '1'); $this->assertNull($user2); } public function testGetByUsername(): void { - $user = $this->object->getByUsername('user2'); + $user = $this->object->get('user2', $this->object->getUserDefinition()->getUsername()); $this->assertEquals($this->prefix . '2', $user->getUserid()); $this->assertEquals('User 2', $user->getName()); @@ -321,12 +324,12 @@ public function testGetByUsername(): void public function testGetByUserProperty(): void { // Add property to user1 - $user = $this->object->getById($this->prefix . '1'); + $user = $this->object->get($this->prefix . '1'); $user->set('property1', 'somevalue'); $this->object->save($user); // Add property to user2 - $user = $this->object->getById($this->prefix . '2'); + $user = $this->object->get($this->prefix . '2'); $user->set('property1', 'value1'); $user->set('property2', 'value2'); $this->object->save($user); diff --git a/tests/UsersDBDatasetByUsernameTest.php b/tests/UsersDBDatasetByUsernameTest.php index ac2bebe..4430db8 100644 --- a/tests/UsersDBDatasetByUsernameTest.php +++ b/tests/UsersDBDatasetByUsernameTest.php @@ -87,7 +87,7 @@ public function testAddUser() $login = $this->__chooseValue('john', 'johndoe@gmail.com'); - $user = $this->object->getByLoginField($login); + $user = $this->object->get($login, $this->object->getUserDefinition()->loginField()); $this->assertEquals('4', $user->getUserid()); $this->assertEquals('John Doe', $user->getName()); $this->assertEquals('john', $user->getUsername()); @@ -100,7 +100,7 @@ public function testAddUser() $user->setAdmin('y'); $this->object->save($user); - $user2 = $this->object->getByLoginField($login); + $user2 = $this->object->get($login, $this->object->getUserDefinition()->loginField()); $this->assertEquals('y', $user2->getAdmin()); } @@ -157,9 +157,9 @@ public function testWithUpdateValue() $newObject->addUser('User 4', 'user4', 'user4@gmail.com', 'pwd4'); - $login = $this->__chooseValue(']user4[', '-user4@gmail.com-'); + $login = $this->__chooseValue('user4', 'user4@gmail.com'); - $user = $newObject->getByLoginField($login); + $user = $newObject->get($login, $newObject->getUserDefinition()->loginField()); $this->assertEquals('4', $user->getUserid()); $this->assertEquals('([User 4])', $user->getName()); $this->assertEquals(')]user4[(', $user->getUsername()); @@ -174,10 +174,10 @@ public function testWithUpdateValue() #[\Override] public function testSaveAndSave() { - $user = $this->object->getById("1"); + $user = $this->object->get("1"); $this->object->save($user); - $user2 = $this->object->getById("1"); + $user2 = $this->object->get("1"); $this->assertEquals($user, $user2); } diff --git a/tests/UsersDBDatasetDefinitionTest.php b/tests/UsersDBDatasetDefinitionTest.php index 91240fa..c7c0472 100644 --- a/tests/UsersDBDatasetDefinitionTest.php +++ b/tests/UsersDBDatasetDefinitionTest.php @@ -117,7 +117,7 @@ public function testAddUser() $login = $this->__chooseValue('john', 'johndoe@gmail.com'); - $user = $this->object->getByLoginField($login); + $user = $this->object->get($login, $this->object->getUserDefinition()->loginField()); $this->assertEquals('4', $user->getUserid()); $this->assertEquals('John Doe', $user->getName()); $this->assertEquals('john', $user->getUsername()); @@ -132,7 +132,7 @@ public function testAddUser() $user->setAdmin('y'); $this->object->save($user); - $user2 = $this->object->getByLoginField($login); + $user2 = $this->object->get($login, $this->object->getUserDefinition()->loginField()); $this->assertEquals('y', $user2->getAdmin()); } @@ -190,9 +190,9 @@ public function testWithUpdateValue() new MyUserModel('User 4', 'user4@gmail.com', 'user4', 'pwd4', 'no', 'other john') ); - $login = $this->__chooseValue(']user4[', '-user4@gmail.com-'); + $login = $this->__chooseValue('user4', 'user4@gmail.com'); - $user = $newObject->getByLoginField($login); + $user = $newObject->get($login, $newObject->getUserDefinition()->loginField()); $this->assertEquals('4', $user->getUserid()); $this->assertEquals('([User 4])', $user->getName()); $this->assertEquals(')]user4[(', $user->getUsername()); From bdebf45822f8c363d4c8c0c271ee1921d953480d Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Sat, 15 Nov 2025 14:48:28 -0500 Subject: [PATCH 18/40] Remove `UsersAnyDataset` and related tests, migrate example and documentation to `UsersDBDataset` for database-focused user management implementation. --- README.md | 12 +- docs/authentication.md | 2 +- docs/custom-fields.md | 6 +- docs/database-storage.md | 36 +- docs/examples.md | 2 +- docs/jwt-tokens.md | 2 +- docs/password-validation.md | 2 +- docs/session-context.md | 2 +- docs/user-management.md | 30 +- docs/user-properties.md | 4 +- example.php | 12 +- src/Interfaces/UsersInterface.php | 26 +- src/UsersAnyDataset.php | 350 ------------------ src/UsersBase.php | 76 ++-- src/UsersDBDataset.php | 33 +- ...etByUsernameTest.php => TestUsersBase.php} | 104 ++---- tests/UsersAnyDataset2ByEmailTest.php | 14 - tests/UsersAnyDataset2ByUsernameTest.php | 59 --- tests/UsersAnyDatasetByEmailTest.php | 14 - tests/UsersDBDataset2ByEmailTest.php | 2 +- ...sersDBDataset2ByUserNameTestUsersBase.php} | 65 +++- tests/UsersDBDatasetByEmailTest.php | 2 +- ...UsersDBDatasetByUsernameTestUsersBase.php} | 14 +- tests/UsersDBDatasetDefinitionTest.php | 10 +- 24 files changed, 211 insertions(+), 668 deletions(-) delete mode 100644 src/UsersAnyDataset.php rename tests/{UsersAnyDatasetByUsernameTest.php => TestUsersBase.php} (73%) delete mode 100644 tests/UsersAnyDataset2ByEmailTest.php delete mode 100644 tests/UsersAnyDataset2ByUsernameTest.php delete mode 100644 tests/UsersAnyDatasetByEmailTest.php rename tests/{UsersDBDataset2ByUserNameTest.php => UsersDBDataset2ByUserNameTestUsersBase.php} (56%) rename tests/{UsersDBDatasetByUsernameTest.php => UsersDBDatasetByUsernameTestUsersBase.php} (92%) diff --git a/README.md b/README.md index a45a321..ad0a61e 100644 --- a/README.md +++ b/README.md @@ -95,13 +95,11 @@ Because this project uses PHP Session you need to run the unit test the followin │ UserPropertyDefinition │─ ─ ┘ └───────────────────┘ ─ ─ ┤ UserPropertyModel │ └────────────────────────┘ ▲ └────────────────────────┘ │ - ┌────────────────────────┼─────────────────────────┐ - │ │ │ - │ │ │ - │ │ │ - ┌───────────────────┐ ┌───────────────────┐ ┌────────────────────┐ - │ UsersAnyDataset │ │ UsersDBDataset │ │ Custom Impl. │ - └───────────────────┘ └───────────────────┘ └────────────────────┘ + ┌───────────┴───────────┐ + │ │ + ┌───────────────────┐ ┌────────────────────┐ + │ UsersDBDataset │ │ Custom Impl. │ + └───────────────────┘ └────────────────────┘ ``` ## License diff --git a/docs/authentication.md b/docs/authentication.md index 7ba0842..8a52ed4 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -133,7 +133,7 @@ $sessionContext = new SessionContext(Factory::createSessionPool()); if ($sessionContext->isAuthenticated()) { $userId = $sessionContext->userInfo(); - $user = $users->getById($userId); + $user = $users->get($userId); echo "Hello, " . $user->getName(); } else { echo "Please log in"; diff --git a/docs/custom-fields.md b/docs/custom-fields.md index 050c6e8..4312bb6 100644 --- a/docs/custom-fields.md +++ b/docs/custom-fields.md @@ -166,7 +166,7 @@ $users->save($user); ```php getById($userId); +$user = $users->get($userId); // Access custom fields echo $user->getName(); @@ -179,7 +179,7 @@ echo $user->getTitle(); ```php getById($userId); +$user = $users->get($userId); $user->setDepartment('Sales'); $user->setTitle('Sales Manager'); $users->save($user); @@ -357,7 +357,7 @@ $user->setTitle('Marketing Director'); $savedUser = $users->save($user); // Retrieve and update -$user = $users->getById($savedUser->getUserid()); +$user = $users->get($savedUser->getUserid()); $user->setTitle('VP of Marketing'); $users->save($user); ``` diff --git a/docs/database-storage.md b/docs/database-storage.md index 8e9c872..4e00ea3 100644 --- a/docs/database-storage.md +++ b/docs/database-storage.md @@ -148,7 +148,7 @@ $userDefinition = new UserDefinition( ``` :::tip Login Field -The login field affects methods like `isValidUser()` and `getByLoginField()`. They will use the configured field for authentication. +The login field affects methods like `isValidUser()`. They will use the configured field for authentication. ::: ## Complete Example @@ -196,27 +196,6 @@ $users = new UsersDBDataset($dbDriver, $userDefinition, $propertiesDefinition); $user = $users->addUser('John Doe', 'johndoe', 'john@example.com', 'password123'); ``` -## XML/File Storage - -For simple applications or development, you can use XML file storage: - -```php -addUser('John Doe', 'johndoe', 'john@example.com', 'password123'); -``` - -:::warning Production Use -XML file storage is suitable for development and small applications. For production applications with multiple users, use database storage. -::: ## Architecture @@ -232,18 +211,15 @@ XML file storage is suitable for development and small applications. For product │ UserPropertyDefinition │─ ─ ┘ └───────────────────┘ ─ ─ ┤ UserPropertyModel │ └────────────────────────┘ ▲ └────────────────────────┘ │ - ┌────────────────────────┼─────────────────────────┐ - │ │ │ - │ │ │ - │ │ │ - ┌───────────────────┐ ┌───────────────────┐ ┌────────────────────┐ - │ UsersAnyDataset │ │ UsersDBDataset │ │ Custom Impl. │ - └───────────────────┘ └───────────────────┘ └────────────────────┘ + ┌───────────┴───────────┐ + │ │ + ┌───────────────────┐ ┌────────────────────┐ + │ UsersDBDataset │ │ Custom Impl. │ + └───────────────────┘ └────────────────────┘ ``` - **UserInterface**: Base interface for all implementations - **UsersDBDataset**: Database implementation -- **UsersAnyDataset**: XML file implementation - **UserModel**: The user data model - **UserPropertyModel**: The user property data model - **UserDefinition**: Maps model to database schema diff --git a/docs/examples.md b/docs/examples.md index 184c36e..0d04a97 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -203,7 +203,7 @@ if (!$sessionContext->isAuthenticated()) { // Get current user $userId = $sessionContext->userInfo(); -$user = $users->getById($userId); +$user = $users->get($userId); $loginTime = $sessionContext->getSessionData('login_time'); ?> diff --git a/docs/jwt-tokens.md b/docs/jwt-tokens.md index 0993d28..02cdf2d 100644 --- a/docs/jwt-tokens.md +++ b/docs/jwt-tokens.md @@ -285,7 +285,7 @@ if (preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) { $jwtData = $jwtWrapper->extractData($token); $username = $jwtData->data['login'] ?? null; - $user = $users->getByLoginField($username); + $user = $users->get($username, $users->getUserDefinition()->loginField()); if ($user !== null) { $users->removeProperty($user->getUserid(), 'TOKEN_HASH'); } diff --git a/docs/password-validation.md b/docs/password-validation.md index 8df36b7..1306ac4 100644 --- a/docs/password-validation.md +++ b/docs/password-validation.md @@ -253,7 +253,7 @@ Repeated patterns include: getById($userId); + $user = $users->get($userId); $user->withPasswordDefinition($passwordDefinition); // Verify old password diff --git a/docs/session-context.md b/docs/session-context.md index d0f9290..d361105 100644 --- a/docs/session-context.md +++ b/docs/session-context.md @@ -169,7 +169,7 @@ if (!$sessionContext->isAuthenticated()) { } $userId = $sessionContext->userInfo(); -$user = $users->getById($userId); +$user = $users->get($userId); $loginTime = $sessionContext->getSessionData('login_time'); echo "Welcome, " . $user->getName(); diff --git a/docs/user-management.md b/docs/user-management.md index 5a4456c..cd749c6 100644 --- a/docs/user-management.md +++ b/docs/user-management.md @@ -41,34 +41,48 @@ $savedUser = $users->save($userModel); ## Retrieving Users +To retrieve users you can use the `get($value, ?string $field = null)` method. +When the `$field` argument is omitted it defaults to the primary key +defined in your `UserDefinition`. Passing any other column automatically builds the right filter and throws an +`\InvalidArgumentException` if the field is not one of the allowed values (`userid`, `username`, or `email`). + +```php +get('john@example.com', $users->getUserDefinition()->getEmail()); +``` + +The following examples show the common calls: + ### Get User by ID ```php getById($userId); +$user = $users->get($userId); +# OR +$user = $users->get($userId, $users->getUserDefinition()->getUserid()); ``` ### Get User by Email ```php getByEmail('john@example.com'); +$user = $users->get('john@example.com', $users->getUserDefinition()->getEmail()); ``` ### Get User by Username ```php getByUsername('johndoe'); +$user = $users->get('johndoe', $users->getUserDefinition()->getUsername()); ``` ### Get User by Login Field -The login field is determined by the `UserDefinition` (either email or username): +The login field is determined by the `UserDefinition::loginField()` (either email or username): ```php getByLoginField('johndoe'); +$user = $users->get('johndoe', $users->getUserDefinition()->loginField()); ``` ### Using Custom Filters @@ -92,7 +106,7 @@ $user = $users->getUser($filter); ```php getById($userId); +$user = $users->get($userId); // Update fields $user->setName('Jane Doe'); @@ -120,6 +134,10 @@ $users->removeByLoginField('johndoe'); ## Checking Admin Status +The admin flag is now interpreted entirely inside `UserModel`. Use `$user->isAdmin()` to read the computed boolean value, +and `$user->setAdmin(true)` (or one of the accepted string values) to change it. This replaces the old `$users->isAdmin()` +method that lived in `UsersInterface`. + ```php getById($userId); +$user = $users->get($userId); // Set a property value $user->set('phone', '555-1234'); @@ -88,7 +88,7 @@ Returns `null` if the property doesn't exist. ```php getById($userId); +$user = $users->get($userId); // Get property value(s) $phone = $user->get('phone'); diff --git a/example.php b/example.php index 49c2da6..64924d1 100644 --- a/example.php +++ b/example.php @@ -2,16 +2,16 @@ require "vendor/autoload.php"; -use ByJG\Authenticate\UsersAnyDataset; +use ByJG\AnyDataset\Db\Factory as DbFactory; use ByJG\Authenticate\SessionContext; -use ByJG\AnyDataset\Core\AnyDataset; +use ByJG\Authenticate\UsersDBDataset; use ByJG\Cache\Factory; -// Create or load AnyDataset from XML file -$anyDataset = new AnyDataset('/tmp/users.xml'); +// Create database connection (using SQLite for this example) +$dbDriver = DbFactory::getDbInstance('sqlite:///tmp/users.db'); // Initialize user management -$users = new UsersAnyDataset($anyDataset); +$users = new UsersDBDataset($dbDriver); // Add a new user $user = $users->addUser('Some User Full Name', 'someuser', 'someuser@someemail.com', '12345'); @@ -35,7 +35,7 @@ $session->setSessionData('login_time', time()); // Get the user info - $currentUser = $users->getById($session->userInfo()); + $currentUser = $users->get($session->userInfo()); echo "Welcome, " . $currentUser->getName() . "\n"; } diff --git a/src/Interfaces/UsersInterface.php b/src/Interfaces/UsersInterface.php index aa52622..2facd33 100644 --- a/src/Interfaces/UsersInterface.php +++ b/src/Interfaces/UsersInterface.php @@ -50,31 +50,11 @@ public function getUser(IteratorFilter $filter): UserModel|null; /** * Enter description here... * - * @param string|HexUuidLiteral|int $userid + * @param string|HexUuidLiteral|int $value + * @param string|null $field * @return UserModel|null */ - public function getById(string|HexUuidLiteral|int $userid): UserModel|null; - - /** - * @desc Get the user based on his email. - * @param string $email Email to find - * @return UserModel|null if user was found; null, otherwise - */ - public function getByEmail(string $email): UserModel|null; - - /** - * @desc Get the user based on his username. - * @param string $username - * @return UserModel|null if user was found; null, otherwise - */ - public function getByUsername(string $username): UserModel|null; - - /** - * @desc Get the user based on his login - * @param string $login - * @return UserModel|null if user was found; null, otherwise - */ - public function getByLoginField(string $login): UserModel|null; + public function get(string|HexUuidLiteral|int $value, ?string $field = null): UserModel|null; /** * @desc Remove the user based on his login. diff --git a/src/UsersAnyDataset.php b/src/UsersAnyDataset.php deleted file mode 100644 index e184362..0000000 --- a/src/UsersAnyDataset.php +++ /dev/null @@ -1,350 +0,0 @@ -anyDataSet = $anyDataset; - $this->anyDataSet->save(); - $this->userTable = $userTable; - if (!$userTable->existsMapper('update', UserDefinition::FIELD_USERID)) { - $userTable->defineMapperForUpdate(UserDefinition::FIELD_USERID, UserIdGeneratorMapper::class); - } - $this->propertiesTable = $propertiesTable; - } - - /** - * Save the current UsersAnyDataset - * - * @param UserModel $model - * @return UserModel - * @throws DatabaseException - * @throws InvalidArgumentException - * @throws UserExistsException - * @throws FileException - */ - #[Override] - 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) { - $mapper = $this->getUserDefinition()->getMapperForUpdate($property); - if (is_string($mapper)) { - $mapper = new $mapper(); - } - $value = $mapper->processedValue($model->{"get$property"}(), $model); - if ($value !== false) { - $this->anyDataSet->addField($map, $value); - } - } - - // Group properties by name to handle multiple values - $propertiesByName = []; - foreach ($model->getProperties() as $property) { - $name = $property->getName(); - if (!isset($propertiesByName[$name])) { - $propertiesByName[$name] = []; - } - $propertiesByName[$name][] = $property->getValue(); - } - - // Add properties, using array if multiple values exist - foreach ($propertiesByName as $name => $values) { - if (count($values) === 1) { - $this->anyDataSet->addField($name, $values[0]); - } else { - $this->anyDataSet->addField($name, $values); - } - } - - $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 - */ - #[Override] - public function getUser(IteratorFilter $filter): UserModel|null - { - $iterator = $this->anyDataSet->getIterator($filter); - if (!$iterator->valid()) { - return null; - } - - return $this->createUserModel($iterator->current()); - } - - /** - * 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 - */ - #[Override] - 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->valid()) { - $oldRow = $iterator->current(); - $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|null $filter = null): IteratorInterface - { - return $this->anyDataSet->getIterator($filter); - } - - /** - * @param string $propertyName - * @param string $value - * @return array - */ - #[Override] - public function getUsersByProperty(string $propertyName, string $value): array - { - return $this->getUsersByPropertySet([$propertyName => $value]); - } - - /** - * @param array $propertiesArray - * @return array - */ - #[Override] - 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 - */ - #[Override] - 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 - */ - #[Override] - 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 - */ - #[Override] - 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 - */ - #[Override] - public function removeAllProperties(string $propertyName, string|null $value = null): void - { - $iterator = $this->getIterator(); - foreach ($iterator as $user) { - //anydataset.Row - $this->removeProperty($user->get($this->getUserDefinition()->getUserid()), $propertyName, $value); - } - } - - /** - * @param RowInterface $row - * @return UserModel - */ - private function createUserModel(RowInterface $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) { - $values = $row->get($property); - - // Handle both single values and arrays - if (!is_array($values)) { - if ($values !== null) { - $userModel->addProperty(new UserPropertiesModel($property, $values)); - } - } else { - foreach ($values as $eachValue) { - $userModel->addProperty(new UserPropertiesModel($property, $eachValue)); - } - } - } - - return $userModel; - } - - /** - * @param string|HexUuidLiteral|int $userid - * @return bool - * @throws InvalidArgumentException - */ - #[Override] - 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->valid()) { - $oldRow = $iterator->current(); - $this->anyDataSet->removeRow($oldRow); - return true; - } - - return false; - } -} diff --git a/src/UsersBase.php b/src/UsersBase.php index 5e08667..d2deadc 100644 --- a/src/UsersBase.php +++ b/src/UsersBase.php @@ -59,7 +59,7 @@ public function getUserPropertiesDefinition(): UserPropertiesDefinition } /** - * Save the current UsersAnyDataset + * Save the current user * * @param UserModel $model */ @@ -96,7 +96,7 @@ public function addUser(string $name, string $userName, string $email, string $p #[Override] public function canAddUser(UserModel $model): bool { - if ($this->getByEmail($model->getEmail()) !== null) { + if (!empty($model->getUserid()) && $this->get($model->getUserid()) !== null) { throw new UserExistsException('Email already exists'); } $filter = new IteratorFilter(); @@ -119,66 +119,32 @@ public function canAddUser(UserModel $model): bool 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 - */ - #[Override] - 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. + * Get the user based on his id. * Return Row if user was found; null, otherwise * - * @param string $username + * @param string|HexUuidLiteral|int $value + * @param string|null $field * @return UserModel|null * @throws InvalidArgumentException */ #[Override] - public function getByUsername(string $username): UserModel|null + public function get(string|HexUuidLiteral|int $value, ?string $field = null): 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 - */ - #[Override] - public function getByLoginField(string $login): UserModel|null - { - $filter = new IteratorFilter(); - $filter->and($this->getUserDefinition()->loginField(), Relation::EQUAL, strtolower($login)); + if (empty($field)) { + $field = $this->getUserDefinition()->getUserid(); + } - return $this->getUser($filter); - } + $validFields = [ + $this->getUserDefinition()->getUserid(), + $this->getUserDefinition()->getUsername(), + $this->getUserDefinition()->getEmail() + ]; - /** - * 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 - */ - #[Override] - public function getById(string|HexUuidLiteral|int $userid): UserModel|null - { + if (!in_array($field, $validFields)) { + throw new \InvalidArgumentException("Invalid field type provided. Should be one of " . implode(", ", $validFields)); + } $filter = new IteratorFilter(); - $filter->and($this->getUserDefinition()->getUserid(), Relation::EQUAL, $userid); + $filter->and($field, Relation::EQUAL, $value); return $this->getUser($filter); } @@ -236,7 +202,7 @@ public function hasProperty(string|int|HexUuidLiteral|null $userId, string $prop if (is_string($userIdMapper)) { $userIdMapper = new $userIdMapper(); } - $user = $this->getById($userIdMapper->processedValue($userId, null)); + $user = $this->get($userIdMapper->processedValue($userId, null)); if (empty($user)) { return false; @@ -271,7 +237,7 @@ public function hasProperty(string|int|HexUuidLiteral|null $userId, string $prop #[Override] public function getProperty(string|HexUuidLiteral|int $userId, string $propertyName): array|string|UserPropertiesModel|null { - $user = $this->getById($userId); + $user = $this->get($userId); if ($user !== null) { $values = $user->get($propertyName); @@ -382,7 +348,7 @@ public function createAuthToken( #[Override] public function isValidToken(string $login, JwtWrapper $jwtWrapper, string $token): array|null { - $user = $this->getByLoginField($login); + $user = $this->get($login, $this->getUserDefinition()->loginField()); if (is_null($user)) { throw new UserNotFoundException('User not found!'); diff --git a/src/UsersDBDataset.php b/src/UsersDBDataset.php index f3cce7b..c8d4c05 100644 --- a/src/UsersDBDataset.php +++ b/src/UsersDBDataset.php @@ -123,7 +123,7 @@ public function __construct( } /** - * Save the current UsersAnyDataset + * Save the current user * * @param UserModel $model * @return UserModel @@ -152,7 +152,7 @@ public function save(UserModel $model): UserModel } if ($newUser) { - $model = $this->getById($model->getUserid()); + $model = $this->get($model->getUserid()); } if ($model === null) { @@ -162,6 +162,29 @@ public function save(UserModel $model): UserModel return $model; } + #[\Override] + public function get(string|HexUuidLiteral|int $value, ?string $field = null): ?UserModel + { + if (empty($field)) { + $field = $this->getUserDefinition()->getUserid(); + } + + $function = match ($field) { + $this->getUserDefinition()->getEmail() => $this->getUserDefinition()->getMapperForUpdate(UserDefinition::FIELD_EMAIL), + $this->getUserDefinition()->getUsername() => $this->getUserDefinition()->getMapperForUpdate(UserDefinition::FIELD_USERNAME), + default => $this->getUserDefinition()->getMapperForUpdate(UserDefinition::FIELD_USERID), + }; + + if (!empty($function)) { + if (is_string($function)) { + $function = new $function(); + } + $value = $function->processedValue($value, null); + } + + return parent::get($value, $field); + } + /** * Get the users database information based on a filter. * @@ -228,7 +251,7 @@ public function getUser(IteratorFilter $filter): UserModel|null #[Override] public function removeByLoginField(string $login): bool { - $user = $this->getByLoginField($login); + $user = $this->get($login, $this->getUserDefinition()->loginField()); if ($user !== null) { return $this->removeUserById($user->getUserid()); @@ -327,7 +350,7 @@ public function getUsersByPropertySet(array $propertiesArray): array public function addProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value): bool { //anydataset.Row - $user = $this->getById($userId); + $user = $this->get($userId); if (empty($user)) { return false; } @@ -396,7 +419,7 @@ public function setProperty(string|HexUuidLiteral|int $userId, string $propertyN #[Override] public function removeProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value = null): bool { - $user = $this->getById($userId); + $user = $this->get($userId); if ($user !== null) { $updateable = DeleteQuery::getInstance() diff --git a/tests/UsersAnyDatasetByUsernameTest.php b/tests/TestUsersBase.php similarity index 73% rename from tests/UsersAnyDatasetByUsernameTest.php rename to tests/TestUsersBase.php index 8e336d2..136448d 100644 --- a/tests/UsersAnyDatasetByUsernameTest.php +++ b/tests/TestUsersBase.php @@ -2,19 +2,15 @@ namespace Tests; -use ByJG\AnyDataset\Core\AnyDataset; use ByJG\Authenticate\Definition\UserDefinition; -use ByJG\Authenticate\Definition\UserPropertiesDefinition; use ByJG\Authenticate\Exception\NotAuthenticatedException; use ByJG\Authenticate\Exception\UserExistsException; -use ByJG\Authenticate\Model\UserModel; -use ByJG\Authenticate\UsersAnyDataset; use ByJG\Authenticate\UsersBase; use ByJG\JwtWrapper\JwtHashHmacSecret; use ByJG\JwtWrapper\JwtWrapper; use PHPUnit\Framework\TestCase; -class UsersAnyDatasetByUsernameTest extends TestCase +abstract class TestUsersBase extends TestCase { /** * @var UsersBase|null @@ -34,25 +30,9 @@ class UsersAnyDatasetByUsernameTest extends TestCase protected $prefix = ""; - public function __setUp($loginField) - { - $this->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'); - } + abstract public function __setUp($loginField); - public function __chooseValue($forUsername, $forEmail) + public function __chooseValue($forUsername, $forEmail): string { $searchForList = [ $this->userDefinition->getUsername() => $forUsername, @@ -70,18 +50,7 @@ public function setUp(): void /** * @return void */ - 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()); - } + abstract public function testAddUser(); public function testAddUserError(): void { @@ -92,17 +61,17 @@ public function testAddUserError(): void public function testAddProperty(): void { // Check state - $user = $this->object->getById($this->prefix . '2'); + $user = $this->object->get($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'); + $user = $this->object->get($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'); + $user = $this->object->get($this->prefix . '2'); $this->assertEquals(['Rio de Janeiro', 'Belo Horizonte'], $user->get('city')); // Get Property @@ -110,12 +79,12 @@ public function testAddProperty(): void // Add another property $this->object->addProperty($this->prefix . '2', 'state', 'RJ'); - $user = $this->object->getById($this->prefix . '2'); + $user = $this->object->get($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'); + $user = $this->object->get($this->prefix . '2'); $this->assertEmpty($user->get('state')); // Remove Property Again @@ -130,32 +99,32 @@ public function testRemoveAllProperties(): void $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'); + $user = $this->object->get($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'); + $user = $this->object->get($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'); + $user = $this->object->get($this->prefix . '2'); $this->assertEquals(['Rio de Janeiro', 'Niteroi'], $user->get('city')); $this->assertEmpty($user->get('state')); - $user = $this->object->getById($this->prefix . '1'); + $user = $this->object->get($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'); + $user = $this->object->get($this->prefix . '2'); $this->assertEquals('Rio de Janeiro', $user->get('city')); $this->assertEmpty($user->get('state')); - $user = $this->object->getById($this->prefix . '1'); + $user = $this->object->get($this->prefix . '1'); $this->assertEmpty($user->get('city')); $this->assertEmpty($user->get('state')); @@ -165,13 +134,13 @@ public function testRemoveByLoginField(): void { $login = $this->__chooseValue('user1', 'user1@gmail.com'); - $user = $this->object->getByLoginField($login); + $user = $this->object->get($login, $this->object->getUserDefinition()->loginField()); $this->assertNotNull($user); $result = $this->object->removeByLoginField($login); $this->assertTrue($result); - $user = $this->object->getByLoginField($login); + $user = $this->object->get($login, $this->object->getUserDefinition()->loginField()); $this->assertNull($user); } @@ -180,7 +149,7 @@ public function testEditUser(): void $login = $this->__chooseValue('user1', 'user1@gmail.com'); // Getting data - $user = $this->object->getByLoginField($login); + $user = $this->object->get($login, $this->object->getUserDefinition()->loginField()); $this->assertEquals('User 1', $user->getName()); // Change and Persist data @@ -188,7 +157,7 @@ public function testEditUser(): void $this->object->save($user); // Check if data persists - $user = $this->object->getById($this->prefix . '1'); + $user = $this->object->get($this->prefix . '1'); $this->assertEquals('Other name', $user->getName()); } @@ -209,17 +178,17 @@ public function testIsValidUser(): void public function testIsAdmin(): void { // Check is Admin - $user3 = $this->object->getById($this->prefix . '3'); + $user3 = $this->object->get($this->prefix . '3'); $this->assertFalse($user3->isAdmin()); // Set the Admin Flag $login = $this->__chooseValue('user3', 'user3@gmail.com'); - $user = $this->object->getByLoginField($login); + $user = $this->object->get($login, $this->object->getUserDefinition()->loginField()); $user->setAdmin('Y'); $this->object->save($user); // Check is Admin - $user3 = $this->object->getById($this->prefix . '3'); + $user3 = $this->object->get($this->prefix . '3'); $this->assertTrue($user3->isAdmin()); } @@ -238,7 +207,7 @@ protected function expectedToken($tokenData, $login, $userId): void ['tokenData'=>$tokenData] ); - $user = $this->object->getByLoginField($login); + $user = $this->object->get($login, $this->object->getUserDefinition()->loginField()); $dataFromToken = new \stdClass(); $dataFromToken->tokenData = $tokenData; @@ -257,12 +226,7 @@ protected function expectedToken($tokenData, $login, $userId): void /** * @return void */ - public function testCreateAuthToken() - { - $login = $this->__chooseValue('user2', 'user2@gmail.com'); - - $this->expectedToken('tokenValue', $login, 'user2'); - } + abstract public function testCreateAuthToken(); public function testValidateTokenWithAnotherUser(): void { @@ -286,30 +250,22 @@ public function testValidateTokenWithAnotherUser(): void /** * @return void */ - public function testSaveAndSave() - { - $user = $this->object->getById('user1'); - $this->object->save($user); - - $user2 = $this->object->getById('user1'); - - $this->assertEquals($user, $user2); - } + abstract public function testSaveAndSave(); public function testRemoveUserById(): void { - $user = $this->object->getById($this->prefix . '1'); + $user = $this->object->get($this->prefix . '1'); $this->assertNotNull($user); $this->object->removeUserById($this->prefix . '1'); - $user2 = $this->object->getById($this->prefix . '1'); + $user2 = $this->object->get($this->prefix . '1'); $this->assertNull($user2); } public function testGetByUsername(): void { - $user = $this->object->getByUsername('user2'); + $user = $this->object->get('user2', $this->object->getUserDefinition()->getUsername()); $this->assertEquals($this->prefix . '2', $user->getUserid()); $this->assertEquals('User 2', $user->getName()); @@ -321,12 +277,12 @@ public function testGetByUsername(): void public function testGetByUserProperty(): void { // Add property to user1 - $user = $this->object->getById($this->prefix . '1'); + $user = $this->object->get($this->prefix . '1'); $user->set('property1', 'somevalue'); $this->object->save($user); // Add property to user2 - $user = $this->object->getById($this->prefix . '2'); + $user = $this->object->get($this->prefix . '2'); $user->set('property1', 'value1'); $user->set('property2', 'value2'); $this->object->save($user); diff --git a/tests/UsersAnyDataset2ByEmailTest.php b/tests/UsersAnyDataset2ByEmailTest.php deleted file mode 100644 index 2091e71..0000000 --- a/tests/UsersAnyDataset2ByEmailTest.php +++ /dev/null @@ -1,14 +0,0 @@ -__setUp(UserDefinition::LOGIN_IS_EMAIL); - } -} diff --git a/tests/UsersAnyDataset2ByUsernameTest.php b/tests/UsersAnyDataset2ByUsernameTest.php deleted file mode 100644 index 50eaa86..0000000 --- a/tests/UsersAnyDataset2ByUsernameTest.php +++ /dev/null @@ -1,59 +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'); - } - - #[\Override] - 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 b7ad3bf..0000000 --- a/tests/UsersAnyDatasetByEmailTest.php +++ /dev/null @@ -1,14 +0,0 @@ -__setUp(UserDefinition::LOGIN_IS_EMAIL); - } -} diff --git a/tests/UsersDBDataset2ByEmailTest.php b/tests/UsersDBDataset2ByEmailTest.php index 8a33475..a97c5c3 100644 --- a/tests/UsersDBDataset2ByEmailTest.php +++ b/tests/UsersDBDataset2ByEmailTest.php @@ -4,7 +4,7 @@ use ByJG\Authenticate\Definition\UserDefinition; -class UsersDBDataset2ByEmailTest extends UsersDBDatasetByUsernameTest +class UsersDBDataset2ByEmailTest extends UsersDBDataset2ByUserNameTestUsersBase { #[\Override] public function setUp(): void diff --git a/tests/UsersDBDataset2ByUserNameTest.php b/tests/UsersDBDataset2ByUserNameTestUsersBase.php similarity index 56% rename from tests/UsersDBDataset2ByUserNameTest.php rename to tests/UsersDBDataset2ByUserNameTestUsersBase.php index a13cc78..5480fda 100644 --- a/tests/UsersDBDataset2ByUserNameTest.php +++ b/tests/UsersDBDataset2ByUserNameTestUsersBase.php @@ -9,8 +9,10 @@ use ByJG\Authenticate\UsersDBDataset; use ByJG\MicroOrm\Exception\OrmModelInvalidException; -class UsersDBDataset2ByUserNameTest extends UsersDBDatasetByUsernameTest +class UsersDBDataset2ByUserNameTestUsersBase extends TestUsersBase { + const CONNECTION_STRING='sqlite:///tmp/teste.db'; + protected $db; /** @@ -74,4 +76,65 @@ public function setUp(): void { $this->__setUp(UserDefinition::LOGIN_IS_USERNAME); } + + #[\Override] + public function tearDown(): void + { + $uri = new \ByJG\Util\Uri(self::CONNECTION_STRING); + unlink($uri->getPath()); + $this->object = null; + $this->userDefinition = null; + $this->propertyDefinition = 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->get($login, $this->object->getUserDefinition()->loginField()); + $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->get($login, $this->object->getUserDefinition()->loginField()); + $this->assertEquals('y', $user2->getAdmin()); + } + + /** + * @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->get("1"); + $this->object->save($user); + + $user2 = $this->object->get("1"); + + $this->assertEquals($user, $user2); + } } diff --git a/tests/UsersDBDatasetByEmailTest.php b/tests/UsersDBDatasetByEmailTest.php index e37efe5..83a3f0a 100644 --- a/tests/UsersDBDatasetByEmailTest.php +++ b/tests/UsersDBDatasetByEmailTest.php @@ -4,7 +4,7 @@ use ByJG\Authenticate\Definition\UserDefinition; -class UsersDBDatasetByEmailTest extends UsersAnyDatasetByUsernameTest +class UsersDBDatasetByEmailTest extends UsersDBDatasetByUsernameTestUsersBase { #[\Override] public function setUp(): void diff --git a/tests/UsersDBDatasetByUsernameTest.php b/tests/UsersDBDatasetByUsernameTestUsersBase.php similarity index 92% rename from tests/UsersDBDatasetByUsernameTest.php rename to tests/UsersDBDatasetByUsernameTestUsersBase.php index ac2bebe..df015df 100644 --- a/tests/UsersDBDatasetByUsernameTest.php +++ b/tests/UsersDBDatasetByUsernameTestUsersBase.php @@ -10,7 +10,7 @@ use ByJG\Authenticate\UsersDBDataset; use ByJG\Util\Uri; -class UsersDBDatasetByUsernameTest extends UsersAnyDatasetByUsernameTest +class UsersDBDatasetByUsernameTestUsersBase extends TestUsersBase { const CONNECTION_STRING='sqlite:///tmp/teste.db'; @@ -87,7 +87,7 @@ public function testAddUser() $login = $this->__chooseValue('john', 'johndoe@gmail.com'); - $user = $this->object->getByLoginField($login); + $user = $this->object->get($login, $this->object->getUserDefinition()->loginField()); $this->assertEquals('4', $user->getUserid()); $this->assertEquals('John Doe', $user->getName()); $this->assertEquals('john', $user->getUsername()); @@ -100,7 +100,7 @@ public function testAddUser() $user->setAdmin('y'); $this->object->save($user); - $user2 = $this->object->getByLoginField($login); + $user2 = $this->object->get($login, $this->object->getUserDefinition()->loginField()); $this->assertEquals('y', $user2->getAdmin()); } @@ -157,9 +157,9 @@ public function testWithUpdateValue() $newObject->addUser('User 4', 'user4', 'user4@gmail.com', 'pwd4'); - $login = $this->__chooseValue(']user4[', '-user4@gmail.com-'); + $login = $this->__chooseValue('user4', 'user4@gmail.com'); - $user = $newObject->getByLoginField($login); + $user = $newObject->get($login, $newObject->getUserDefinition()->loginField()); $this->assertEquals('4', $user->getUserid()); $this->assertEquals('([User 4])', $user->getName()); $this->assertEquals(')]user4[(', $user->getUsername()); @@ -174,10 +174,10 @@ public function testWithUpdateValue() #[\Override] public function testSaveAndSave() { - $user = $this->object->getById("1"); + $user = $this->object->get("1"); $this->object->save($user); - $user2 = $this->object->getById("1"); + $user2 = $this->object->get("1"); $this->assertEquals($user, $user2); } diff --git a/tests/UsersDBDatasetDefinitionTest.php b/tests/UsersDBDatasetDefinitionTest.php index 91240fa..187dafe 100644 --- a/tests/UsersDBDatasetDefinitionTest.php +++ b/tests/UsersDBDatasetDefinitionTest.php @@ -19,7 +19,7 @@ use Tests\Fixture\MyUserModel; use Tests\Fixture\TestUniqueIdGenerator; -class UsersDBDatasetDefinitionTest extends UsersDBDatasetByUsernameTest +class UsersDBDatasetDefinitionTest extends UsersDBDatasetByUsernameTestUsersBase { protected $db; @@ -117,7 +117,7 @@ public function testAddUser() $login = $this->__chooseValue('john', 'johndoe@gmail.com'); - $user = $this->object->getByLoginField($login); + $user = $this->object->get($login, $this->object->getUserDefinition()->loginField()); $this->assertEquals('4', $user->getUserid()); $this->assertEquals('John Doe', $user->getName()); $this->assertEquals('john', $user->getUsername()); @@ -132,7 +132,7 @@ public function testAddUser() $user->setAdmin('y'); $this->object->save($user); - $user2 = $this->object->getByLoginField($login); + $user2 = $this->object->get($login, $this->object->getUserDefinition()->loginField()); $this->assertEquals('y', $user2->getAdmin()); } @@ -190,9 +190,9 @@ public function testWithUpdateValue() new MyUserModel('User 4', 'user4@gmail.com', 'user4', 'pwd4', 'no', 'other john') ); - $login = $this->__chooseValue(']user4[', '-user4@gmail.com-'); + $login = $this->__chooseValue('user4', 'user4@gmail.com'); - $user = $newObject->getByLoginField($login); + $user = $newObject->get($login, $newObject->getUserDefinition()->loginField()); $this->assertEquals('4', $user->getUserid()); $this->assertEquals('([User 4])', $user->getName()); $this->assertEquals(')]user4[(', $user->getUsername()); From 3b4552da650e800d927b18c4e718d78fc1e2f655 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Sat, 15 Nov 2025 15:49:32 -0500 Subject: [PATCH 19/40] Migrate to a repository-based architecture: replace `UsersAnyDataset` with `UsersRepository`, add `UserPropertiesRepository`, implement `UsersService` and interface for business logic, update `UserModel` and `UserPropertiesModel` with attributes for ORM integration, and enhance property management and user authentication workflows. --- src/Model/UserModel.php | 18 + src/Model/UserPropertiesModel.php | 10 + src/Repository/UserPropertiesRepository.php | 204 +++++++++ src/Repository/UsersRepository.php | 152 +++++++ src/Service/UsersService.php | 433 ++++++++++++++++++++ src/Service/UsersServiceInterface.php | 195 +++++++++ src/UsersAnyDataset.php | 0 7 files changed, 1012 insertions(+) create mode 100644 src/Repository/UserPropertiesRepository.php create mode 100644 src/Repository/UsersRepository.php create mode 100644 src/Service/UsersService.php create mode 100644 src/Service/UsersServiceInterface.php delete mode 100644 src/UsersAnyDataset.php diff --git a/src/Model/UserModel.php b/src/Model/UserModel.php index 4209cff..96c70ed 100644 --- a/src/Model/UserModel.php +++ b/src/Model/UserModel.php @@ -3,17 +3,35 @@ namespace ByJG\Authenticate\Model; use ByJG\Authenticate\Definition\PasswordDefinition; +use ByJG\Authenticate\MapperFunctions\PasswordSha1Mapper; +use ByJG\MicroOrm\Attributes\FieldAttribute; +use ByJG\MicroOrm\Attributes\TableAttribute; use ByJG\MicroOrm\Literal\HexUuidLiteral; +use ByJG\MicroOrm\MapperFunctions\ReadOnlyMapper; use InvalidArgumentException; +#[TableAttribute(tableName: 'users')] class UserModel { + #[FieldAttribute(primaryKey: true)] protected string|int|HexUuidLiteral|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; + + #[FieldAttribute(updateFunction: ReadOnlyMapper::class)] protected ?string $created = null; + + #[FieldAttribute] protected ?string $admin = null; protected ?PasswordDefinition $passwordDefinition = null; diff --git a/src/Model/UserPropertiesModel.php b/src/Model/UserPropertiesModel.php index 2d38c66..7c60eee 100644 --- a/src/Model/UserPropertiesModel.php +++ b/src/Model/UserPropertiesModel.php @@ -2,13 +2,23 @@ namespace ByJG\Authenticate\Model; +use ByJG\MicroOrm\Attributes\FieldAttribute; +use ByJG\MicroOrm\Attributes\TableAttribute; use ByJG\MicroOrm\Literal\HexUuidLiteral; +#[TableAttribute(tableName: 'users_property')] class UserPropertiesModel { + #[FieldAttribute] protected string|int|HexUuidLiteral|null $userid = null; + + #[FieldAttribute(primaryKey: true)] protected ?string $id = null; + + #[FieldAttribute] protected ?string $name = null; + + #[FieldAttribute] protected ?string $value = null; /** diff --git a/src/Repository/UserPropertiesRepository.php b/src/Repository/UserPropertiesRepository.php new file mode 100644 index 0000000..f116710 --- /dev/null +++ b/src/Repository/UserPropertiesRepository.php @@ -0,0 +1,204 @@ +mapper = new Mapper($propertiesClass); + $this->repository = new Repository($executor, $propertiesClass); + } + + /** + * Save a property + * + * @param UserPropertiesModel $model + * @return UserPropertiesModel + * @throws OrmBeforeInvalidException + * @throws OrmInvalidFieldsException + */ + public function save(UserPropertiesModel $model): UserPropertiesModel + { + $this->repository->save($model); + return $model; + } + + /** + * Get properties by user ID + * + * @param string|HexUuidLiteral|int $userid + * @return UserPropertiesModel[] + * @throws DatabaseException + * @throws DbDriverNotConnected + * @throws FileException + * @throws XmlUtilException + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function getByUserId(string|HexUuidLiteral|int $userid): array + { + $useridField = $this->mapper->getFieldMap('userid')->getFieldName(); + $query = Query::getInstance() + ->table($this->mapper->getTable()) + ->where("$useridField = :userid", ['userid' => $userid]); + + return $this->repository->getByQuery($query); + } + + /** + * Get specific property by user ID and name + * + * @param string|HexUuidLiteral|int $userid + * @param string $propertyName + * @return UserPropertiesModel[] + * @throws DatabaseException + * @throws DbDriverNotConnected + * @throws FileException + * @throws XmlUtilException + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function getByUserIdAndName(string|HexUuidLiteral|int $userid, string $propertyName): array + { + $useridField = $this->mapper->getFieldMap('userid')->getFieldName(); + $nameField = $this->mapper->getFieldMap('name')->getFieldName(); + + $query = Query::getInstance() + ->table($this->mapper->getTable()) + ->where("$useridField = :userid", ['userid' => $userid]) + ->where("$nameField = :name", ['name' => $propertyName]); + + return $this->repository->getByQuery($query); + } + + /** + * Delete all properties for a user + * + * @param string|HexUuidLiteral|int $userid + * @return void + * @throws DatabaseException + * @throws DbDriverNotConnected + * @throws RepositoryReadOnlyException + */ + public function deleteByUserId(string|HexUuidLiteral|int $userid): void + { + $useridField = $this->mapper->getFieldMap('userid')->getFieldName(); + + $deleteQuery = DeleteQuery::getInstance() + ->table($this->mapper->getTable()) + ->where("$useridField = :userid", ['userid' => $userid]); + + $this->repository->deleteByQuery($deleteQuery); + } + + /** + * Delete specific property by user ID and name + * + * @param string|HexUuidLiteral|int $userid + * @param string $propertyName + * @param string|null $value + * @return void + * @throws DatabaseException + * @throws DbDriverNotConnected + * @throws RepositoryReadOnlyException + */ + public function deleteByUserIdAndName(string|HexUuidLiteral|int $userid, string $propertyName, ?string $value = null): void + { + $useridField = $this->mapper->getFieldMap('userid')->getFieldName(); + $nameField = $this->mapper->getFieldMap('name')->getFieldName(); + $valueField = $this->mapper->getFieldMap('value')->getFieldName(); + + $deleteQuery = DeleteQuery::getInstance() + ->table($this->mapper->getTable()) + ->where("$useridField = :userid", ['userid' => $userid]) + ->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('name')->getFieldName(); + $valueField = $this->mapper->getFieldMap('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..8ebed5d --- /dev/null +++ b/src/Repository/UsersRepository.php @@ -0,0 +1,152 @@ +mapper = new Mapper($usersClass); + $this->repository = new Repository($executor, $usersClass); + } + + /** + * Save a user + * + * @param UserModel $model + * @return UserModel + * @throws OrmBeforeInvalidException + * @throws OrmInvalidFieldsException + */ + public function save(UserModel $model): UserModel + { + $this->repository->save($model); + return $model; + } + + /** + * Get user by ID + * + * @param string|HexUuidLiteral|int $userid + * @return UserModel|null + */ + public function getById(string|HexUuidLiteral|int $userid): ?UserModel + { + return $this->repository->get($userid); + } + + /** + * Get user by field value + * + * @param string $field + * @param string|HexUuidLiteral|int $value + * @return UserModel|null + * @throws DatabaseException + * @throws DbDriverNotConnected + * @throws FileException + * @throws XmlUtilException + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function getByField(string $field, string|HexUuidLiteral|int $value): ?UserModel + { + $query = Query::getInstance() + ->table($this->mapper->getTable()) + ->where("$field = :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|HexUuidLiteral|int $userid + * @return void + * @throws \Exception + */ + public function deleteById(string|HexUuidLiteral|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 + */ + public function getPrimaryKeyName(): string + { + 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..23f70b2 --- /dev/null +++ b/src/Service/UsersService.php @@ -0,0 +1,433 @@ +usersRepository = $usersRepository; + $this->propertiesRepository = $propertiesRepository; + $this->loginField = $loginField; + $this->passwordDefinition = $passwordDefinition; + } + + /** + * @inheritDoc + */ + 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; + } + + /** + * @inheritDoc + */ + public function addUser(string $name, string $userName, string $email, string $password): UserModel + { + + $model = $this->usersRepository->getMapper()->getEntity([ + 'name' => $name, + 'email' => $email, + 'username' => $userName, + 'password' => $password + ]); + if ($this->passwordDefinition !== null) { + $model->withPasswordDefinition($this->passwordDefinition); + } + + 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 + */ + public function getById(string|HexUuidLiteral|int $userid): ?UserModel + { + $user = $this->usersRepository->getById($userid); + if ($user !== null) { + $this->loadUserProperties($user); + } + return $user; + } + + /** + * @inheritDoc + */ + public function getByEmail(string $email): ?UserModel + { + $user = $this->usersRepository->getByField('email', $email); + if ($user !== null) { + $this->loadUserProperties($user); + } + return $user; + } + + /** + * @inheritDoc + */ + public function getByUsername(string $username): ?UserModel + { + $user = $this->usersRepository->getByField('username', $username); + if ($user !== null) { + $this->loadUserProperties($user); + } + return $user; + } + + /** + * @inheritDoc + */ + public function getByLogin(string $login): ?UserModel + { + return $this->loginField === self::LOGIN_IS_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); + } + + /** + * @inheritDoc + */ + public function removeByLogin(string $login): bool + { + $user = $this->getByLogin($login); + if ($user !== null) { + return $this->removeById($user->getUserid()); + } + return false; + } + + /** + * @inheritDoc + */ + public function removeById(string|HexUuidLiteral|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 + */ + public function isValidUser(string $login, string $password): ?UserModel + { + $user = $this->getByLogin($login); + + if ($user === null) { + return null; + } + + // Hash the password for comparison + $passwordMapper = new PasswordSha1Mapper(); + $hashedPassword = $passwordMapper->processedValue($password, null); + + if ($user->getPassword() === $hashedPassword) { + return $user; + } + + return null; + } + + /** + * @inheritDoc + */ + public function hasProperty(string|int|HexUuidLiteral $userId, string $propertyName, ?string $value = null): bool + { + $user = $this->getById($userId); + + if (empty($user)) { + return false; + } + + if ($user->isAdmin()) { + return true; + } + + $values = $user->get($propertyName); + + if ($values === null) { + return false; + } + + if ($value === null) { + return true; + } + + return in_array($value, (array)$values); + } + + /** + * @inheritDoc + */ + public function getProperty(string|HexUuidLiteral|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 + */ + public function addProperty(string|HexUuidLiteral|int $userId, string $propertyName, ?string $value): bool + { + $user = $this->getById($userId); + if (empty($user)) { + return false; + } + + if (!$this->hasProperty($userId, $propertyName, $value)) { + $property = $this->propertiesRepository->getMapper()->getEntity([ + 'userid' => $userId, + 'name' => $propertyName, + 'value' => $value + ]); + $this->propertiesRepository->save($property); + } + + return true; + } + + /** + * @inheritDoc + */ + public function setProperty(string|HexUuidLiteral|int $userId, string $propertyName, ?string $value): bool + { + $properties = $this->propertiesRepository->getByUserIdAndName($userId, $propertyName); + + if (empty($properties)) { + $property = $this->propertiesRepository->getMapper()->getEntity([ + 'userid' => $userId, + 'name' => $propertyName, + 'value' => $value + ]); + } else { + $property = $properties[0]; + $property->setValue($value); + } + + $this->propertiesRepository->save($property); + return true; + } + + /** + * @inheritDoc + */ + public function removeProperty(string|HexUuidLiteral|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 + */ + public function removeAllProperties(string $propertyName, ?string $value = null): void + { + $this->propertiesRepository->deleteByName($propertyName, $value); + } + + /** + * @inheritDoc + */ + public function getUsersByProperty(string $propertyName, string $value): array + { + return $this->getUsersByPropertySet([$propertyName => $value]); + } + + /** + * @inheritDoc + */ + public function getUsersByPropertySet(array $propertiesArray): array + { + $userTable = $this->usersRepository->getTableName(); + $propTable = $this->propertiesRepository->getTableName(); + $userPk = $this->usersRepository->getPrimaryKeyName(); + + $propUserIdField = $this->propertiesRepository->getMapper()->getFieldMap('userid')->getFieldName(); + $propNameField = $this->propertiesRepository->getMapper()->getFieldMap('name')->getFieldName(); + $propValueField = $this->propertiesRepository->getMapper()->getFieldMap('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]); + } + + return $this->usersRepository->getRepository()->getByQuery($query); + } + + /** + * @inheritDoc + */ + public function createAuthToken( + string $login, + string $password, + JwtWrapper $jwtWrapper, + int $expires = 1200, + array $updateUserInfo = [], + array $updateTokenInfo = [] + ): ?string { + $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['userid'] = $user->getUserid(); + $jwtData = $jwtWrapper->createJwtData($updateTokenInfo, $expires); + + $token = $jwtWrapper->generateToken($jwtData); + + $user->set('TOKEN_HASH', sha1($token)); + $this->save($user); + + return $token; + } + + /** + * @inheritDoc + * @throws JwtWrapperException + * @throws NotAuthenticatedException + * @throws UserNotFoundException + */ + public function isValidToken(string $login, JwtWrapper $jwtWrapper, string $token): ?array + { + $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 [ + 'user' => $user, + 'data' => $data->data + ]; + } +} diff --git a/src/Service/UsersServiceInterface.php b/src/Service/UsersServiceInterface.php new file mode 100644 index 0000000..5c339dc --- /dev/null +++ b/src/Service/UsersServiceInterface.php @@ -0,0 +1,195 @@ + Date: Sat, 15 Nov 2025 16:18:58 -0500 Subject: [PATCH 20/40] Remove deprecated user-related classes: delete `UserDefinition`, `UserPropertiesDefinition`, `UsersBase`, and `UsersDBDataset` to fully transition to repository-based architecture. --- README.md | 47 +- docs/authentication.md | 9 +- docs/custom-fields.md | 213 +++---- docs/database-storage.md | 214 +++---- docs/examples.md | 56 +- docs/getting-started.md | 19 +- docs/user-management.md | 50 +- example.php | 18 +- src/Definition/UserDefinition.php | 336 ----------- src/Definition/UserPropertiesDefinition.php | 76 --- src/Interfaces/UsersInterface.php | 168 ------ src/Repository/UsersRepository.php | 12 +- src/Service/UsersService.php | 34 +- src/UsersBase.php | 376 ------------- src/UsersDBDataset.php | 521 ------------------ tests/CustomUserModel.php | 34 ++ tests/CustomUserPropertiesModel.php | 23 + tests/Fixture/MyUserModel.php | 28 + tests/Fixture/MyUserPropertiesModel.php | 23 + tests/Fixture/UserModelMd5.php | 33 ++ tests/PasswordMd5MapperTest.php | 71 ++- tests/TestUsersBase.php | 261 +++++++-- tests/UsersDBDataset2ByEmailTest.php | 4 +- ...UsersDBDataset2ByUserNameTestUsersBase.php | 74 +-- tests/UsersDBDatasetByEmailTest.php | 4 +- .../UsersDBDatasetByUsernameTestUsersBase.php | 112 ++-- tests/UsersDBDatasetDefinitionTest.php | 231 ++------ 27 files changed, 849 insertions(+), 2198 deletions(-) delete mode 100644 src/Definition/UserDefinition.php delete mode 100644 src/Definition/UserPropertiesDefinition.php delete mode 100644 src/Interfaces/UsersInterface.php delete mode 100644 src/UsersBase.php delete mode 100644 src/UsersDBDataset.php create mode 100644 tests/CustomUserModel.php create mode 100644 tests/CustomUserPropertiesModel.php create mode 100644 tests/Fixture/MyUserPropertiesModel.php create mode 100644 tests/Fixture/UserModelMd5.php diff --git a/README.md b/README.md index ad0a61e..fc84d4b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![GitHub license](https://img.shields.io/github/license/byjg/php-authuser.svg)](https://opensource.byjg.com/opensource/licensing.html) [![GitHub release](https://img.shields.io/github/release/byjg/php-authuser.svg)](https://github.com/byjg/php-authuser/releases/) -A simple and customizable library for user authentication in PHP applications. It supports multiple storage backends including databases and XML files. +A simple and customizable library for user authentication in PHP applications using a clean repository and service layer architecture. 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. @@ -40,13 +40,22 @@ See [Installation Guide](docs/installation.md) for detailed setup instructions a ```php addUser('John Doe', 'johndoe', 'john@example.com', 'SecurePass123'); @@ -68,7 +77,7 @@ See [Getting Started](docs/getting-started.md) for a complete introduction and [ - **Session Management** - PSR-6 compatible cache storage. See [Session Context](docs/session-context.md) - **User Properties** - Store custom key-value metadata. See [User Properties](docs/user-properties.md) - **Password Validation** - Built-in strength requirements. See [Password Validation](docs/password-validation.md) -- **Multiple Storage** - Database (MySQL, PostgreSQL, SQLite, etc.) or XML files. See [Database Storage](docs/database-storage.md) +- **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) @@ -88,18 +97,22 @@ Because this project uses PHP Session you need to run the unit test the followin │ SessionContext │ └───────────────────┘ │ -┌────────────────────────┐ ┌────────────────────────┐ -│ UserDefinition │─ ─ ┐ │ ─ ─ ┤ UserModel │ -└────────────────────────┘ ┌───────────────────┐ │ └────────────────────────┘ -┌────────────────────────┐ └────│ UsersInterface │────┐ ┌────────────────────────┐ -│ UserPropertyDefinition │─ ─ ┘ └───────────────────┘ ─ ─ ┤ UserPropertyModel │ -└────────────────────────┘ ▲ └────────────────────────┘ │ - ┌───────────┴───────────┐ - │ │ - ┌───────────────────┐ ┌────────────────────┐ - │ UsersDBDataset │ │ Custom Impl. │ - └───────────────────┘ └────────────────────┘ + ┌───────────────────┐ + │ UsersService │ (Business Logic) + └───────────────────┘ + │ + ┌────────────────────┴────────────────────┐ + │ │ + ┌───────────────────┐ ┌──────────────────────┐ + │ UsersRepository │ │ PropertiesRepository │ + └───────────────────┘ └──────────────────────┘ + │ │ + ┌───────┴───────┐ ┌──────────┴──────────┐ + │ │ │ │ + ┌───────────────┐ ┌────────┐ ┌───────────────┐ ┌──────────────┐ + │ UserModel │ │ Mapper │ │ PropsModel │ │ Mapper │ + └───────────────┘ └────────┘ └───────────────┘ └──────────────┘ ``` ## License diff --git a/docs/authentication.md b/docs/authentication.md index 8a52ed4..9344557 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -22,7 +22,7 @@ if ($user !== null) { ``` :::tip Login Field -The `isValidUser()` method uses the login field defined in your `UserDefinition`. This can be either the email or username field. +The `isValidUser()` method uses the login field configured in your `UsersService` constructor. This can be either `UsersService::LOGIN_IS_EMAIL` or `UsersService::LOGIN_IS_USERNAME`. ::: ## Password Hashing @@ -52,12 +52,11 @@ For modern, stateless authentication, use JWT tokens. This is the **recommended ```php createAuthToken( @@ -133,7 +132,7 @@ $sessionContext = new SessionContext(Factory::createSessionPool()); if ($sessionContext->isAuthenticated()) { $userId = $sessionContext->userInfo(); - $user = $users->get($userId); + $user = $users->getById($userId); echo "Hello, " . $user->getName(); } else { echo "Please log in"; diff --git a/docs/custom-fields.md b/docs/custom-fields.md index 4312bb6..2900c90 100644 --- a/docs/custom-fields.md +++ b/docs/custom-fields.md @@ -20,12 +20,20 @@ This guide is for **adding new fields** beyond the standard user fields. If you namespace App\Model; use ByJG\Authenticate\Model\UserModel; +use ByJG\MicroOrm\Attributes\FieldAttribute; class CustomUserModel extends UserModel { + #[FieldAttribute(fieldName: 'phone')] protected ?string $phone = null; + + #[FieldAttribute(fieldName: 'department')] protected ?string $department = null; + + #[FieldAttribute(fieldName: 'title')] protected ?string $title = null; + + #[FieldAttribute(fieldName: 'profile_picture')] protected ?string $profilePicture = null; public function __construct( @@ -108,46 +116,30 @@ CREATE TABLE users ) ENGINE=InnoDB; ``` -## Configuring UserDefinition +## Using the Custom Model -Map the custom fields in your `UserDefinition`: +### Initializing the Service ```php 'userid', - UserDefinition::FIELD_NAME => 'name', - UserDefinition::FIELD_EMAIL => 'email', - UserDefinition::FIELD_USERNAME => 'username', - UserDefinition::FIELD_PASSWORD => 'password', - UserDefinition::FIELD_CREATED => 'created', - UserDefinition::FIELD_ADMIN => 'admin', - // Custom fields - 'phone' => 'phone', - 'department' => 'department', - 'title' => 'title', - 'profilePicture' => 'profile_picture' - ] -); -``` - -## Using the Custom Model +// Database connection +$dbDriver = Factory::getDbInstance('mysql://user:password@localhost/database'); +$db = DatabaseExecutor::using($dbDriver); -### Creating Users +// Initialize repositories with custom model +$usersRepo = new UsersRepository($db, CustomUserModel::class); +$propsRepo = new UserPropertiesRepository($db, UserPropertiesModel::class); -```php -save($user); ```php get($userId); +$user = $users->getById($userId); // Access custom fields echo $user->getName(); @@ -179,7 +171,7 @@ echo $user->getTitle(); ```php get($userId); +$user = $users->getById($userId); $user->setDepartment('Sales'); $user->setTitle('Sales Manager'); $users->save($user); @@ -187,15 +179,23 @@ $users->save($user); ## Read-Only Fields -You can mark fields as read-only to prevent updates: +You can mark fields as read-only to prevent updates using the `ReadOnlyMapper`: ```php markPropertyAsReadOnly(UserDefinition::FIELD_CREATED); +use ByJG\MicroOrm\MapperFunctions\ReadOnlyMapper; +use ByJG\MicroOrm\Attributes\FieldAttribute; + +class CustomUserModel extends UserModel +{ + // Read-only field - can be set on creation but not updated + #[FieldAttribute(fieldName: 'created', updateFunction: ReadOnlyMapper::class)] + protected ?string $created = null; -// Make custom field read-only -$userDefinition->markPropertyAsReadOnly('phone'); + // Read-only custom field + #[FieldAttribute(fieldName: 'phone', updateFunction: ReadOnlyMapper::class)] + protected ?string $phone = null; +} ``` Read-only fields: @@ -203,105 +203,45 @@ Read-only fields: - Cannot be updated after creation - Are ignored during updates -## Auto-Generated Fields - -### Auto-Increment IDs - -For auto-increment IDs, the database handles generation automatically. No configuration needed. - -### UUID Fields - -For UUID primary keys: - -```php -defineGenerateKey(UserIdGeneratorMapper::class); -``` - -### Custom ID Generation - -Create a custom mapper for custom ID generation: - -```php -defineGenerateKey(CustomIdMapper::class); -``` - ## Field Transformation You can transform fields during read/write operations using mappers. See [Mappers](mappers.md) for details. -## Complex Data Types - ### JSON Fields For storing JSON data in custom fields: ```php defineMapperForUpdate('metadata', JsonMapper::class); -$userDefinition->defineMapperForSelect('metadata', JsonDecodeMapper::class); ``` ### Date/Time Fields ```php format('Y-m-d H:i:s'); - } - return $value; - } + #[FieldAttribute( + fieldName: 'created', + selectFunction: [FieldHandler::class, 'toDate'] + )] + protected ?\DateTime $created = null; } - -$userDefinition->defineMapperForUpdate('created', DateTimeMapper::class); ``` ## Complete Example @@ -310,39 +250,24 @@ $userDefinition->defineMapperForUpdate('created', DateTimeMapper::class); 'userid', - UserDefinition::FIELD_NAME => 'name', - UserDefinition::FIELD_EMAIL => 'email', - UserDefinition::FIELD_USERNAME => 'username', - UserDefinition::FIELD_PASSWORD => 'password', - UserDefinition::FIELD_CREATED => 'created', - UserDefinition::FIELD_ADMIN => 'admin', - 'phone' => 'phone', - 'department' => 'department', - 'title' => 'title', - 'profilePicture' => 'profile_picture' - ] -); - -// Make created field read-only -$userDefinition->markPropertyAsReadOnly(UserDefinition::FIELD_CREATED); - -// Initialize user management -$users = new UsersDBDataset($dbDriver, $userDefinition); +// Initialize user service +$users = new UsersService($usersRepo, $propsRepo, UsersService::LOGIN_IS_EMAIL); // Create a user $user = new CustomUserModel(); @@ -357,7 +282,7 @@ $user->setTitle('Marketing Director'); $savedUser = $users->save($user); // Retrieve and update -$user = $users->get($savedUser->getUserid()); +$user = $users->getById($savedUser->getUserid()); $user->setTitle('VP of Marketing'); $users->save($user); ``` diff --git a/docs/database-storage.md b/docs/database-storage.md index 4e00ea3..8f69ffd 100644 --- a/docs/database-storage.md +++ b/docs/database-storage.md @@ -5,7 +5,7 @@ title: Database Storage # Database Storage -The library supports storing users in relational databases through the `UsersDBDataset` class. +The library uses a repository pattern to store users in relational databases through `UsersRepository` and `UserPropertiesRepository`. ## Database Setup @@ -29,12 +29,12 @@ CREATE TABLE users CREATE TABLE users_property ( - customid INTEGER AUTO_INCREMENT NOT NULL, + id INTEGER AUTO_INCREMENT NOT NULL, name VARCHAR(20), value VARCHAR(100), userid INTEGER NOT NULL, - CONSTRAINT pk_custom PRIMARY KEY (customid), + CONSTRAINT pk_custom PRIMARY KEY (id), CONSTRAINT fk_custom_user FOREIGN KEY (userid) REFERENCES users (userid) ) ENGINE=InnoDB; ``` @@ -45,20 +45,24 @@ CREATE TABLE users_property ```php 'user_id', - UserDefinition::FIELD_NAME => 'full_name', - UserDefinition::FIELD_EMAIL => 'email_address', - UserDefinition::FIELD_USERNAME => 'user_name', - UserDefinition::FIELD_PASSWORD => 'password_hash', - UserDefinition::FIELD_CREATED => 'date_created', - UserDefinition::FIELD_ADMIN => 'is_admin' - ] -); - -$users = new UsersDBDataset($dbDriver, $userDefinition); +use ByJG\Authenticate\Model\UserModel; +use ByJG\Authenticate\MapperFunctions\PasswordSha1Mapper; +use ByJG\MicroOrm\Attributes\FieldAttribute; +use ByJG\MicroOrm\Attributes\TableAttribute; +use ByJG\MicroOrm\MapperFunctions\ReadOnlyMapper; +use ByJG\MicroOrm\Literal\HexUuidLiteral; + +#[TableAttribute(tableName: 'my_users_table')] +class CustomUserModel extends UserModel +{ + #[FieldAttribute(fieldName: 'user_id', primaryKey: true)] + protected string|int|HexUuidLiteral|null $userid = null; + + #[FieldAttribute(fieldName: 'full_name')] + protected ?string $name = null; + + #[FieldAttribute(fieldName: 'email_address')] + protected ?string $email = null; + + #[FieldAttribute(fieldName: 'user_name')] + protected ?string $username = null; + + #[FieldAttribute(fieldName: 'password_hash', updateFunction: PasswordSha1Mapper::class)] + protected ?string $password = null; + + #[FieldAttribute(fieldName: 'date_created', updateFunction: ReadOnlyMapper::class)] + protected ?string $created = null; + + #[FieldAttribute(fieldName: 'is_admin')] + protected ?string $admin = null; +} + +// Use custom model +$usersRepo = new UsersRepository($db, CustomUserModel::class); ``` ### Custom Properties Table ```php 'id', - UserDefinition::FIELD_NAME => 'fullname', - UserDefinition::FIELD_EMAIL => 'email', - UserDefinition::FIELD_USERNAME => 'username', - UserDefinition::FIELD_PASSWORD => 'pwd', - UserDefinition::FIELD_CREATED => 'created_at', - UserDefinition::FIELD_ADMIN => 'is_admin' - ] -); - -// Custom properties definition -$propertiesDefinition = new UserPropertiesDefinition( - 'app_user_meta', - 'id', - 'meta_key', - 'meta_value', - 'user_id' -); - -// Initialize -$users = new UsersDBDataset($dbDriver, $userDefinition, $propertiesDefinition); +// Initialize with custom models +$usersRepo = new UsersRepository($db, CustomUserModel::class); +$propsRepo = new UserPropertiesRepository($db, CustomPropertiesModel::class); +$users = new UsersService($usersRepo, $propsRepo, UsersService::LOGIN_IS_EMAIL); // Use it $user = $users->addUser('John Doe', 'johndoe', 'john@example.com', 'password123'); ``` - ## Architecture ```text @@ -204,26 +200,30 @@ $user = $users->addUser('John Doe', 'johndoe', 'john@example.com', 'password123' │ SessionContext │ └───────────────────┘ │ -┌────────────────────────┐ ┌────────────────────────┐ -│ UserDefinition │─ ─ ┐ │ ─ ─ ┤ UserModel │ -└────────────────────────┘ ┌───────────────────┐ │ └────────────────────────┘ -┌────────────────────────┐ └────│ UsersInterface │────┐ ┌────────────────────────┐ -│ UserPropertyDefinition │─ ─ ┘ └───────────────────┘ ─ ─ ┤ UserPropertyModel │ -└────────────────────────┘ ▲ └────────────────────────┘ │ - ┌───────────┴───────────┐ - │ │ - ┌───────────────────┐ ┌────────────────────┐ - │ UsersDBDataset │ │ Custom Impl. │ - └───────────────────┘ └────────────────────┘ + ┌───────────────────┐ + │ UsersService │ (Business Logic) + └───────────────────┘ + │ + ┌────────────────────┴────────────────────┐ + │ │ + ┌───────────────────┐ ┌──────────────────────┐ + │ UsersRepository │ │ PropertiesRepository │ + └───────────────────┘ └──────────────────────┘ + │ │ + ┌───────┴───────┐ ┌──────────┴──────────┐ + │ │ │ │ + ┌───────────────┐ ┌────────┐ ┌───────────────┐ ┌──────────────┐ + │ UserModel │ │ Mapper │ │ PropsModel │ │ Mapper │ + └───────────────┘ └────────┘ └───────────────┘ └──────────────┘ ``` -- **UserInterface**: Base interface for all implementations -- **UsersDBDataset**: Database implementation -- **UserModel**: The user data model -- **UserPropertyModel**: The user property data model -- **UserDefinition**: Maps model to database schema -- **UserPropertiesDefinition**: Maps properties to database schema +- **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 diff --git a/docs/examples.md b/docs/examples.md index 0d04a97..6a87e88 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -16,16 +16,26 @@ This page contains complete, working examples for common use cases. // config.php require_once 'vendor/autoload.php'; -use ByJG\Authenticate\UsersDBDataset; +use ByJG\Authenticate\Service\UsersService; +use ByJG\Authenticate\Repository\UsersRepository; +use ByJG\Authenticate\Repository\UserPropertiesRepository; +use ByJG\Authenticate\Model\UserModel; +use ByJG\Authenticate\Model\UserPropertiesModel; use ByJG\Authenticate\SessionContext; use ByJG\Cache\Factory; use ByJG\AnyDataset\Db\Factory as DbFactory; +use ByJG\AnyDataset\Db\DatabaseExecutor; // Database connection $dbDriver = DbFactory::getDbInstance('mysql://user:password@localhost/myapp'); +$db = DatabaseExecutor::using($dbDriver); -// Initialize user management -$users = new UsersDBDataset($dbDriver); +// Initialize repositories +$usersRepo = new UsersRepository($db, UserModel::class); +$propsRepo = new UserPropertiesRepository($db, UserPropertiesModel::class); + +// Initialize user service +$users = new UsersService($usersRepo, $propsRepo, UsersService::LOGIN_IS_USERNAME); // Initialize session $sessionContext = new SessionContext(Factory::createSessionPool()); @@ -203,7 +213,7 @@ if (!$sessionContext->isAuthenticated()) { // Get current user $userId = $sessionContext->userInfo(); -$user = $users->get($userId); +$user = $users->getById($userId); $loginTime = $sessionContext->getSessionData('login_time'); ?> @@ -253,18 +263,27 @@ exit; // api-config.php require_once 'vendor/autoload.php'; -use ByJG\Authenticate\UsersDBDataset; +use ByJG\Authenticate\Service\UsersService; +use ByJG\Authenticate\Repository\UsersRepository; +use ByJG\Authenticate\Repository\UserPropertiesRepository; +use ByJG\Authenticate\Model\UserModel; +use ByJG\Authenticate\Model\UserPropertiesModel; use ByJG\AnyDataset\Db\Factory as DbFactory; -use ByJG\JwtWrapper\JwtKeySecret; +use ByJG\AnyDataset\Db\DatabaseExecutor; +use ByJG\JwtWrapper\JwtHashHmacSecret; use ByJG\JwtWrapper\JwtWrapper; // Database $dbDriver = DbFactory::getDbInstance('mysql://user:password@localhost/api_db'); -$users = new UsersDBDataset($dbDriver); +$db = DatabaseExecutor::using($dbDriver); + +// Initialize repositories and service +$usersRepo = new UsersRepository($db, UserModel::class); +$propsRepo = new UserPropertiesRepository($db, UserPropertiesModel::class); +$users = new UsersService($usersRepo, $propsRepo, UsersService::LOGIN_IS_USERNAME); // JWT -$jwtKey = new JwtKeySecret(getenv('JWT_SECRET') ?: 'your-secret-key'); -$jwtWrapper = new JwtWrapper($jwtKey); +$jwtWrapper = new JwtWrapper('api.example.com', new JwtHashHmacSecret(getenv('JWT_SECRET') ?: 'your-secret-key')); // Helper function function jsonResponse($data, $statusCode = 200) @@ -396,11 +415,20 @@ try { // multi-tenant-example.php require_once 'vendor/autoload.php'; -use ByJG\Authenticate\UsersDBDataset; +use ByJG\Authenticate\Service\UsersService; +use ByJG\Authenticate\Repository\UsersRepository; +use ByJG\Authenticate\Repository\UserPropertiesRepository; +use ByJG\Authenticate\Model\UserModel; +use ByJG\Authenticate\Model\UserPropertiesModel; use ByJG\AnyDataset\Db\Factory as DbFactory; +use ByJG\AnyDataset\Db\DatabaseExecutor; $dbDriver = DbFactory::getDbInstance('mysql://user:password@localhost/multitenant_db'); -$users = new UsersDBDataset($dbDriver); +$db = DatabaseExecutor::using($dbDriver); + +$usersRepo = new UsersRepository($db, UserModel::class); +$propsRepo = new UserPropertiesRepository($db, UserPropertiesModel::class); +$users = new UsersService($usersRepo, $propsRepo, UsersService::LOGIN_IS_USERNAME); // Add user to organization function addUserToOrganization($users, $userId, $orgId, $role = 'member') @@ -454,13 +482,13 @@ if (hasOrganizationAccess($users, $userId, $orgId)) { // permission-system-example.php require_once 'vendor/autoload.php'; -use ByJG\Authenticate\UsersDBDataset; +use ByJG\Authenticate\Service\UsersService; class PermissionManager { - private $users; + private UsersService $users; - public function __construct(UsersDBDataset $users) + public function __construct(UsersService $users) { $this->users = $users; } diff --git a/docs/getting-started.md b/docs/getting-started.md index fecc7c9..6561ea5 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -5,19 +5,28 @@ title: Getting Started # Getting Started -Auth User PHP is a simple and customizable library for user authentication in PHP applications. It provides an abstraction layer for managing users, authentication, and user properties, supporting multiple storage backends including databases and XML files. +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'); diff --git a/docs/user-management.md b/docs/user-management.md index cd749c6..7e3e49f 100644 --- a/docs/user-management.md +++ b/docs/user-management.md @@ -41,64 +41,56 @@ $savedUser = $users->save($userModel); ## Retrieving Users -To retrieve users you can use the `get($value, ?string $field = null)` method. -When the `$field` argument is omitted it defaults to the primary key -defined in your `UserDefinition`. Passing any other column automatically builds the right filter and throws an -`\InvalidArgumentException` if the field is not one of the allowed values (`userid`, `username`, or `email`). - -```php -get('john@example.com', $users->getUserDefinition()->getEmail()); -``` - -The following examples show the common calls: +The `UsersService` provides several methods to retrieve users: ### Get User by ID ```php get($userId); -# OR -$user = $users->get($userId, $users->getUserDefinition()->getUserid()); +$user = $users->getById($userId); ``` ### Get User by Email ```php get('john@example.com', $users->getUserDefinition()->getEmail()); +$user = $users->getByEmail('john@example.com'); ``` ### Get User by Username ```php get('johndoe', $users->getUserDefinition()->getUsername()); +$user = $users->getByUsername('johndoe'); ``` ### Get User by Login Field -The login field is determined by the `UserDefinition::loginField()` (either email or username): +The login field is determined by the `UsersService` constructor (either email or username): ```php get('johndoe', $users->getUserDefinition()->loginField()); +$user = $users->getByLogin('johndoe'); ``` -### Using Custom Filters +### Using Custom Queries -For advanced queries, use `IteratorFilter`: +For advanced queries, use the repository directly: ```php getUsersRepository(); -$filter = new IteratorFilter(); -$filter->and('email', Relation::EQUAL, 'john@example.com'); -$filter->and('admin', Relation::EQUAL, 'yes'); +// Build custom query +$query = Query::getInstance() + ->table('users') + ->where('email = :email', ['email' => 'john@example.com']) + ->where('admin = :admin', ['admin' => 'yes']); -$user = $users->getUser($filter); +$results = $usersRepo->getRepository()->getByQuery($query); ``` ## Updating Users @@ -106,7 +98,7 @@ $user = $users->getUser($filter); ```php get($userId); +$user = $users->getById($userId); // Update fields $user->setName('Jane Doe'); @@ -122,14 +114,14 @@ $users->save($user); ```php removeUserById($userId); +$users->removeById($userId); ``` ### Delete by Login ```php removeByLoginField('johndoe'); +$users->removeByLogin('johndoe'); ``` ## Checking Admin Status diff --git a/example.php b/example.php index 64924d1..e1657e9 100644 --- a/example.php +++ b/example.php @@ -2,16 +2,26 @@ require "vendor/autoload.php"; +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\Authenticate\UsersDBDataset; use ByJG\Cache\Factory; // Create database connection (using SQLite for this example) $dbDriver = DbFactory::getDbInstance('sqlite:///tmp/users.db'); +$db = DatabaseExecutor::using($dbDriver); -// Initialize user management -$users = new UsersDBDataset($dbDriver); +// Initialize repositories +$usersRepository = new UsersRepository($db, UserModel::class); +$propertiesRepository = new UserPropertiesRepository($db, UserPropertiesModel::class); + +// 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'); @@ -35,7 +45,7 @@ $session->setSessionData('login_time', time()); // Get the user info - $currentUser = $users->get($session->userInfo()); + $currentUser = $users->getById($session->userInfo()); echo "Welcome, " . $currentUser->getName() . "\n"; } diff --git a/src/Definition/UserDefinition.php b/src/Definition/UserDefinition.php deleted file mode 100644 index a927402..0000000 --- a/src/Definition/UserDefinition.php +++ /dev/null @@ -1,336 +0,0 @@ - [], "update" => [] ]; - protected string $__loginField; - protected string $__model; - protected array $__properties = []; - protected MapperFunctionInterface|string|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->defineMapperForUpdate(UserDefinition::FIELD_PASSWORD, PasswordSha1Mapper::class); - - if ($loginField !== self::LOGIN_IS_USERNAME && $loginField !== self::LOGIN_IS_EMAIL) { - throw new InvalidArgumentException('Login field is invalid. '); - } - $this->__loginField = $loginField; - - $this->beforeInsert = new PassThroughEntityProcessor(); - $this->beforeUpdate = new PassThroughEntityProcessor(); - } - - /** - * @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 MapperFunctionInterface|string $mapper - */ - private function updateMapperDef(string $event, string $property, MapperFunctionInterface|string $mapper): void - { - $this->checkProperty($property); - $this->__mappers[$event][$property] = $mapper; - } - - private function getMapperDef(string $event, string $property): MapperFunctionInterface|string - { - $this->checkProperty($property); - - if (!$this->existsMapper($event, $property)) { - return StandardMapper::class; - } - - return $this->__mappers[$event][$property]; - } - - public function existsMapper(string $event, string $property): bool - { - // Event not set - if (!isset($this->__mappers[$event])) { - return false; - } - - // Event is set but there is no property - if (!array_key_exists($property, $this->__mappers[$event])) { - return false; - } - - return true; - } - - /** - * @deprecated Use existsMapper instead - */ - public function existsClosure(string $event, string $property): bool - { - return $this->existsMapper($event, $property); - } - - public function markPropertyAsReadOnly(string $property): void - { - $this->updateMapperDef(self::UPDATE, $property, ReadOnlyMapper::class); - } - - public function defineMapperForUpdate(string $property, MapperFunctionInterface|string $mapper): void - { - $this->updateMapperDef(self::UPDATE, $property, $mapper); - } - - public function defineMapperForSelect(string $property, MapperFunctionInterface|string $mapper): void - { - $this->updateMapperDef(self::SELECT, $property, $mapper); - } - - /** - * @deprecated Use defineMapperForUpdate instead - */ - public function defineClosureForUpdate(string $property, Closure $closure): void - { - $this->updateMapperDef(self::UPDATE, $property, new ClosureMapper($closure)); - } - - /** - * @deprecated Use defineMapperForSelect instead - */ - public function defineClosureForSelect(string $property, Closure $closure): void - { - $this->updateMapperDef(self::SELECT, $property, new ClosureMapper($closure)); - } - - public function getMapperForUpdate(string $property): MapperFunctionInterface|string - { - return $this->getMapperDef(self::UPDATE, $property); - } - - /** - * @deprecated Use getMapperForUpdate instead. Returns a Closure for backward compatibility. - */ - public function getClosureForUpdate(string $property): Closure - { - $mapper = $this->getMapperDef(self::UPDATE, $property); - - // Return a closure that wraps the mapper - return function($value, $instance = null) use ($mapper) { - if (is_string($mapper)) { - $mapper = new $mapper(); - } - return $mapper->processedValue($value, $instance); - }; - } - - /** - * @deprecated Use defineGenerateKey instead - */ - public function defineGenerateKeyClosure(Closure $closure): void - { - throw new InvalidArgumentException('defineGenerateKeyClosure is deprecated. Use defineGenerateKey with MapperFunctionsInterface instead.'); - } - - public function defineGenerateKey(MapperFunctionInterface|string $generator): void - { - $this->__generateKey = $generator; - } - - public function getGenerateKey(): MapperFunctionInterface|string|null - { - return $this->__generateKey; - } - - /** - * @deprecated Use getGenerateKey instead - */ - public function getGenerateKeyClosure(): MapperFunctionInterface|string|null - { - return $this->__generateKey; - } - - public function getMapperForSelect(string $property): MapperFunctionInterface|string - { - return $this->getMapperDef(self::SELECT, $property); - } - - /** - * @deprecated Use getMapperForSelect instead. Returns a Closure for backward compatibility. - * @param $property - * @return Closure - */ - public function getClosureForSelect($property): Closure - { - $mapper = $this->getMapperDef(self::SELECT, $property); - - // Return a closure that wraps the mapper - return function($value, $instance = null) use ($mapper) { - if (is_string($mapper)) { - $mapper = new $mapper(); - } - return $mapper->processedValue($value, $instance); - }; - } - - public function model(): string - { - return $this->__model; - } - - public function modelInstance(): UserModel - { - $model = $this->__model; - return new $model(); - } - - protected EntityProcessorInterface $beforeInsert; - - /** - * @return EntityProcessorInterface - */ - public function getBeforeInsert(): EntityProcessorInterface - { - return $this->beforeInsert; - } - - /** - * @param EntityProcessorInterface|Closure $beforeInsert - */ - public function setBeforeInsert(EntityProcessorInterface|Closure $beforeInsert): void - { - if ($beforeInsert instanceof Closure) { - $beforeInsert = new ClosureEntityProcessor($beforeInsert); - } - $this->beforeInsert = $beforeInsert; - } - - protected EntityProcessorInterface $beforeUpdate; - - /** - * @return EntityProcessorInterface - */ - public function getBeforeUpdate(): EntityProcessorInterface - { - return $this->beforeUpdate; - } - - /** - * @param EntityProcessorInterface|Closure $beforeUpdate - */ - public function setBeforeUpdate(EntityProcessorInterface|Closure $beforeUpdate): void - { - if ($beforeUpdate instanceof Closure) { - $beforeUpdate = new ClosureEntityProcessor($beforeUpdate); - } - $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/Interfaces/UsersInterface.php b/src/Interfaces/UsersInterface.php deleted file mode 100644 index 2facd33..0000000 --- a/src/Interfaces/UsersInterface.php +++ /dev/null @@ -1,168 +0,0 @@ -|null|string String vector with all sites - */ - public function getProperty(string|HexUuidLiteral|int $userId, string $propertyName): array|string|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 we 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/Repository/UsersRepository.php b/src/Repository/UsersRepository.php index 8ebed5d..a1e74cb 100644 --- a/src/Repository/UsersRepository.php +++ b/src/Repository/UsersRepository.php @@ -63,7 +63,7 @@ public function getById(string|HexUuidLiteral|int $userid): ?UserModel /** * Get user by field value * - * @param string $field + * @param string $field Property name (e.g., 'username', 'email') * @param string|HexUuidLiteral|int $value * @return UserModel|null * @throws DatabaseException @@ -74,9 +74,13 @@ public function getById(string|HexUuidLiteral|int $userid): ?UserModel */ public function getByField(string $field, string|HexUuidLiteral|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("$field = :value", ['value' => $value]); + ->where("$dbColumnName = :value", ['value' => $value]); $result = $this->repository->getByQuery($query); return count($result) > 0 ? $result[0] : null; @@ -123,9 +127,9 @@ public function getTableName(): string /** * Get the primary key field name from mapper * - * @return string + * @return string|array */ - public function getPrimaryKeyName(): string + public function getPrimaryKeyName(): string|array { return $this->mapper->getPrimaryKey(); } diff --git a/src/Service/UsersService.php b/src/Service/UsersService.php index 23f70b2..22c89e6 100644 --- a/src/Service/UsersService.php +++ b/src/Service/UsersService.php @@ -6,7 +6,6 @@ use ByJG\Authenticate\Exception\NotAuthenticatedException; use ByJG\Authenticate\Exception\UserExistsException; use ByJG\Authenticate\Exception\UserNotFoundException; -use ByJG\Authenticate\MapperFunctions\PasswordSha1Mapper; use ByJG\Authenticate\Model\UserModel; use ByJG\Authenticate\Model\UserPropertiesModel; use ByJG\Authenticate\Repository\UserPropertiesRepository; @@ -42,6 +41,26 @@ public function __construct( $this->passwordDefinition = $passwordDefinition; } + /** + * Get the users repository + * + * @return UsersRepository + */ + public function getUsersRepository(): UsersRepository + { + return $this->usersRepository; + } + + /** + * Get the properties repository + * + * @return UserPropertiesRepository + */ + public function getPropertiesRepository(): UserPropertiesRepository + { + return $this->propertiesRepository; + } + /** * @inheritDoc */ @@ -82,11 +101,11 @@ public function addUser(string $name, string $userName, string $email, string $p 'name' => $name, 'email' => $email, 'username' => $userName, - 'password' => $password ]); if ($this->passwordDefinition !== null) { $model->withPasswordDefinition($this->passwordDefinition); } + $model->setPassword($password); return $this->save($model); } @@ -208,9 +227,9 @@ public function isValidUser(string $login, string $password): ?UserModel return null; } - // Hash the password for comparison - $passwordMapper = new PasswordSha1Mapper(); - $hashedPassword = $passwordMapper->processedValue($password, null); + // Hash the password for comparison using the model's configured password mapper + $passwordFieldMapping = $this->usersRepository->getMapper()->getFieldMap('password'); + $hashedPassword = $passwordFieldMapping->getUpdateFunctionValue($password, null); if ($user->getPassword() === $hashedPassword) { return $user; @@ -352,6 +371,11 @@ public function getUsersByPropertySet(array $propertiesArray): array $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('userid')->getFieldName(); $propNameField = $this->propertiesRepository->getMapper()->getFieldMap('name')->getFieldName(); $propValueField = $this->propertiesRepository->getMapper()->getFieldMap('value')->getFieldName(); diff --git a/src/UsersBase.php b/src/UsersBase.php deleted file mode 100644 index d2deadc..0000000 --- a/src/UsersBase.php +++ /dev/null @@ -1,376 +0,0 @@ -userTable === null) { - $this->userTable = new UserDefinition(); - } - return $this->userTable; - } - - /** - * @return UserPropertiesDefinition - */ - #[Override] - public function getUserPropertiesDefinition(): UserPropertiesDefinition - { - if ($this->propertiesTable === null) { - $this->propertiesTable = new UserPropertiesDefinition(); - } - return $this->propertiesTable; - } - - /** - * Save the current user - * - * @param UserModel $model - */ - #[Override] - 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 - */ - #[Override] - 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 - */ - #[Override] - public function canAddUser(UserModel $model): bool - { - if (!empty($model->getUserid()) && $this->get($model->getUserid()) !== 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 - * */ - #[Override] - abstract public function getUser(IteratorFilter $filter): UserModel|null; - - /** - * Get the user based on his id. - * Return Row if user was found; null, otherwise - * - * @param string|HexUuidLiteral|int $value - * @param string|null $field - * @return UserModel|null - * @throws InvalidArgumentException - */ - #[Override] - public function get(string|HexUuidLiteral|int $value, ?string $field = null): UserModel|null - { - if (empty($field)) { - $field = $this->getUserDefinition()->getUserid(); - } - - $validFields = [ - $this->getUserDefinition()->getUserid(), - $this->getUserDefinition()->getUsername(), - $this->getUserDefinition()->getEmail() - ]; - - if (!in_array($field, $validFields)) { - throw new \InvalidArgumentException("Invalid field type provided. Should be one of " . implode(", ", $validFields)); - } - $filter = new IteratorFilter(); - $filter->and($field, Relation::EQUAL, $value); - return $this->getUser($filter); - } - - /** - * Remove the user based on his login. - * - * @param string $login - * @return bool - * */ - #[Override] - 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 - */ - #[Override] - public function isValidUser(string $userName, string $password): UserModel|null - { - $filter = new IteratorFilter(); - $passwordMapper = $this->getUserDefinition()->getMapperForUpdate(UserDefinition::FIELD_PASSWORD); - if (is_string($passwordMapper)) { - $passwordMapper = new $passwordMapper(); - } - $filter->and($this->getUserDefinition()->loginField(), Relation::EQUAL, strtolower($userName)); - $filter->and( - $this->getUserDefinition()->getPassword(), - Relation::EQUAL, - $passwordMapper->processedValue($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 - */ - #[Override] - public function hasProperty(string|int|HexUuidLiteral|null $userId, string $propertyName, string|null $value = null): bool - { - //anydataset.Row - $userIdMapper = $this->getUserDefinition()->getMapperForUpdate(UserDefinition::FIELD_USERID); - if (is_string($userIdMapper)) { - $userIdMapper = new $userIdMapper(); - } - $user = $this->get($userIdMapper->processedValue($userId, null)); - - if (empty($user)) { - return false; - } - - if ($user->isAdmin()) { - 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 InvalidArgumentException - */ - #[Override] - public function getProperty(string|HexUuidLiteral|int $userId, string $propertyName): array|string|UserPropertiesModel|null - { - $user = $this->get($userId); - if ($user !== null) { - $values = $user->get($propertyName); - - if ($user->isAdmin()) { - 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 - */ - #[Override] - 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 - * */ - #[Override] - 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 - * */ - #[Override] - abstract 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 the TOKEN or false if you don't. - * @throws UserNotFoundException - * @throws InvalidArgumentException - */ - #[Override] - 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 - */ - #[Override] - public function isValidToken(string $login, JwtWrapper $jwtWrapper, string $token): array|null - { - $user = $this->get($login, $this->getUserDefinition()->loginField()); - - 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 - */ - #[Override] - abstract public function removeUserById(string|HexUuidLiteral|int $userid): bool; -} diff --git a/src/UsersDBDataset.php b/src/UsersDBDataset.php deleted file mode 100644 index c8d4c05..0000000 --- a/src/UsersDBDataset.php +++ /dev/null @@ -1,521 +0,0 @@ -executor = $dbDriver; - - if (empty($userTable)) { - $userTable = new UserDefinition(); - } - - if (empty($propertiesTable)) { - $propertiesTable = new UserPropertiesDefinition(); - } - - $userMapper = new Mapper( - $userTable->model(), - $userTable->table(), - $userTable->getUserid() - ); - $seed = $userTable->getGenerateKey(); - if (!empty($seed)) { - $userMapper->withPrimaryKeySeedFunction($seed); - } - - $propertyDefinition = $userTable->toArray(); - - foreach ($propertyDefinition as $property => $map) { - $userMapper->addFieldMapping(FieldMapping::create($property) - ->withFieldName($map) - ->withUpdateFunction($userTable->getMapperForUpdate($property)) - ->withSelectFunction($userTable->getMapperForSelect($property)) - ); - } - $this->userRepository = new Repository($this->executor, $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->getMapperForUpdate(UserDefinition::FIELD_USERID)) - ->withSelectFunction($userTable->getMapperForSelect(UserDefinition::FIELD_USERID)) - ); - $this->propertiesRepository = new Repository($this->executor, $propertiesMapper); - - $this->userTable = $userTable; - $this->propertiesTable = $propertiesTable; - } - - /** - * Save the current user - * - * @param UserModel $model - * @return UserModel - * @throws UserExistsException - * @throws UserNotFoundException - * @throws OrmBeforeInvalidException - * @throws OrmInvalidFieldsException - * @throws Exception - */ - #[Override] - 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->get($model->getUserid()); - } - - if ($model === null) { - throw new UserNotFoundException("User not found"); - } - - return $model; - } - - #[\Override] - public function get(string|HexUuidLiteral|int $value, ?string $field = null): ?UserModel - { - if (empty($field)) { - $field = $this->getUserDefinition()->getUserid(); - } - - $function = match ($field) { - $this->getUserDefinition()->getEmail() => $this->getUserDefinition()->getMapperForUpdate(UserDefinition::FIELD_EMAIL), - $this->getUserDefinition()->getUsername() => $this->getUserDefinition()->getMapperForUpdate(UserDefinition::FIELD_USERNAME), - default => $this->getUserDefinition()->getMapperForUpdate(UserDefinition::FIELD_USERID), - }; - - if (!empty($function)) { - if (is_string($function)) { - $function = new $function(); - } - $value = $function->processedValue($value, null); - } - - return parent::get($value, $field); - } - - /** - * Get the users database information based on a filter. - * - * @param IteratorFilter|null $filter Filter to find user - * @return UserModel[] - * @throws DatabaseException - * @throws DbDriverNotConnected - * @throws FileException - * @throws XmlUtilException - * @throws \Psr\SimpleCache\InvalidArgumentException - */ - public function getIterator(IteratorFilter|null $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 - * @throws DatabaseException - * @throws DbDriverNotConnected - * @throws FileException - * @throws RepositoryReadOnlyException - * @throws XmlUtilException - * @throws \Psr\SimpleCache\InvalidArgumentException - */ - #[Override] - 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 - */ - #[Override] - public function removeByLoginField(string $login): bool - { - $user = $this->get($login, $this->getUserDefinition()->loginField()); - - 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 - */ - #[Override] - 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 DatabaseException - * @throws DbDriverNotConnected - * @throws ExceptionInvalidArgumentException - * @throws FileException - * @throws InvalidArgumentException - * @throws XmlUtilException - * @throws \Psr\SimpleCache\InvalidArgumentException - */ - #[Override] - 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 DatabaseException - * @throws DbDriverNotConnected - * @throws ExceptionInvalidArgumentException - * @throws FileException - * @throws InvalidArgumentException - * @throws XmlUtilException - * @throws \Psr\SimpleCache\InvalidArgumentException - */ - #[Override] - 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 - */ - #[Override] - public function addProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value): bool - { - //anydataset.Row - $user = $this->get($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; - } - - /** - * @param string|HexUuidLiteral|int $userId - * @param string $propertyName - * @param string|null $value - * @return bool - * @throws DatabaseException - * @throws DbDriverNotConnected - * @throws ExceptionInvalidArgumentException - * @throws FileException - * @throws OrmBeforeInvalidException - * @throws OrmInvalidFieldsException - * @throws RepositoryReadOnlyException - * @throws UpdateConstraintException - * @throws XmlUtilException - * @throws \Psr\SimpleCache\InvalidArgumentException - */ - #[Override] - 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 DatabaseException - * @throws DbDriverNotConnected - * @throws ExceptionInvalidArgumentException - * @throws InvalidArgumentException - * @throws RepositoryReadOnlyException - */ - #[Override] - public function removeProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value = null): bool - { - $user = $this->get($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 DatabaseException - * @throws DbDriverNotConnected - * @throws ExceptionInvalidArgumentException - * @throws RepositoryReadOnlyException - */ - #[Override] - 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); - } - - /** - * @throws XmlUtilException - * @throws DatabaseException - * @throws DbDriverNotConnected - * @throws FileException - * @throws \Psr\SimpleCache\InvalidArgumentException - */ - #[Override] - 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 DatabaseException - * @throws DbDriverNotConnected - * @throws FileException - * @throws RepositoryReadOnlyException - * @throws XmlUtilException - * @throws \Psr\SimpleCache\InvalidArgumentException - */ - protected function setPropertiesInUser(UserModel $userRow): void - { - $value = $this->propertiesRepository - ->getMapper() - ->getFieldMap(UserDefinition::FIELD_USERID) - ->getUpdateFunctionValue($userRow->getUserid(), $userRow, $this->propertiesRepository->getExecutorWrite()); - $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..3137397 --- /dev/null +++ b/tests/CustomUserModel.php @@ -0,0 +1,34 @@ +userDefinition = new UserDefinition('users', UserModel::class, UserDefinition::LOGIN_IS_USERNAME); - $this->userDefinition->defineMapperForUpdate(UserDefinition::FIELD_PASSWORD, PasswordMd5Mapper::class); - $this->userDefinition->markPropertyAsReadOnly(UserDefinition::FIELD_CREATED); - - $this->propertyDefinition = new UserPropertiesDefinition(); + // 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, + UsersService::LOGIN_IS_USERNAME + ); } #[\Override] @@ -56,17 +60,14 @@ public function tearDown(): void unlink($uri->getPath()); } $this->db = null; - $this->userDefinition = null; - $this->propertyDefinition = null; + $this->service = null; } public function testPasswordIsHashedWithMd5OnSave(): void { - $dataset = new UsersDBDataset($this->db, $this->userDefinition, $this->propertyDefinition); - // Add a user with a plain text password $plainPassword = 'mySecretPassword123'; - $user = $dataset->addUser('John Doe', 'johndoe', 'john@example.com', $plainPassword); + $user = $this->service->addUser('John Doe', 'johndoe', 'john@example.com', $plainPassword); // Verify the password was hashed with MD5 $expectedHash = md5($plainPassword); @@ -77,15 +78,13 @@ public function testPasswordIsHashedWithMd5OnSave(): void public function testPasswordIsNotRehashedIfAlreadyMd5(): void { - $dataset = new UsersDBDataset($this->db, $this->userDefinition, $this->propertyDefinition); - // Create a user - $user = $dataset->addUser('Jane Doe', 'janedoe', 'jane@example.com', 'password123'); + $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 = $dataset->save($user); + $updatedUser = $this->service->save($user); // Password hash should remain the same $this->assertEquals($originalHash, $updatedUser->getPassword()); @@ -93,16 +92,14 @@ public function testPasswordIsNotRehashedIfAlreadyMd5(): void public function testPasswordIsHashedWhenUpdating(): void { - $dataset = new UsersDBDataset($this->db, $this->userDefinition, $this->propertyDefinition); - // Create a user - $user = $dataset->addUser('Jane Doe', 'janedoe', 'jane@example.com', 'oldPassword'); + $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 = $dataset->save($user); + $updatedUser = $this->service->save($user); // Verify the new password was hashed with MD5 $expectedNewHash = md5($newPlainPassword); @@ -112,28 +109,26 @@ public function testPasswordIsHashedWhenUpdating(): void $this->assertNotEquals($oldHash, $updatedUser->getPassword()); // Verify user can login with new password - $authenticatedUser = $dataset->isValidUser('janedoe', $newPlainPassword); + $authenticatedUser = $this->service->isValidUser('janedoe', $newPlainPassword); $this->assertNotNull($authenticatedUser); // Verify user cannot login with old password - $authenticatedUserOld = $dataset->isValidUser('janedoe', 'oldPassword'); + $authenticatedUserOld = $this->service->isValidUser('janedoe', 'oldPassword'); $this->assertNull($authenticatedUserOld); } public function testPasswordRemainsUnchangedWhenUpdatingOtherFields(): void { - $dataset = new UsersDBDataset($this->db, $this->userDefinition, $this->propertyDefinition); - // Create a user $originalPassword = 'myPassword123'; - $user = $dataset->addUser('John Smith', 'johnsmith', 'john@example.com', $originalPassword); + $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->setAdmin('y'); - $updatedUser = $dataset->save($user); + $updatedUser = $this->service->save($user); // Verify the password hash remained exactly the same $this->assertEquals($originalHash, $updatedUser->getPassword()); @@ -145,21 +140,19 @@ public function testPasswordRemainsUnchangedWhenUpdatingOtherFields(): void $this->assertEquals('y', $updatedUser->getAdmin()); // Verify user can still login with original password - $authenticatedUser = $dataset->isValidUser('johnsmith', $originalPassword); + $authenticatedUser = $this->service->isValidUser('johnsmith', $originalPassword); $this->assertNotNull($authenticatedUser); $this->assertEquals('John Updated', $authenticatedUser->getName()); } public function testUserCanLoginWithMd5HashedPassword(): void { - $dataset = new UsersDBDataset($this->db, $this->userDefinition, $this->propertyDefinition); - // Add a user $plainPassword = 'testPassword456'; - $dataset->addUser('Test User', 'testuser', 'test@example.com', $plainPassword); + $this->service->addUser('Test User', 'testuser', 'test@example.com', $plainPassword); // Verify user can login with the plain text password - $authenticatedUser = $dataset->isValidUser('testuser', $plainPassword); + $authenticatedUser = $this->service->isValidUser('testuser', $plainPassword); $this->assertNotNull($authenticatedUser); $this->assertEquals('Test User', $authenticatedUser->getName()); @@ -168,13 +161,11 @@ public function testUserCanLoginWithMd5HashedPassword(): void public function testUserCannotLoginWithWrongPassword(): void { - $dataset = new UsersDBDataset($this->db, $this->userDefinition, $this->propertyDefinition); - // Add a user - $dataset->addUser('Test User', 'testuser', 'test@example.com', 'correctPassword'); + $this->service->addUser('Test User', 'testuser', 'test@example.com', 'correctPassword'); // Try to login with wrong password - $authenticatedUser = $dataset->isValidUser('testuser', 'wrongPassword'); + $authenticatedUser = $this->service->isValidUser('testuser', 'wrongPassword'); $this->assertNull($authenticatedUser); } diff --git a/tests/TestUsersBase.php b/tests/TestUsersBase.php index 136448d..535f82d 100644 --- a/tests/TestUsersBase.php +++ b/tests/TestUsersBase.php @@ -2,10 +2,9 @@ namespace Tests; -use ByJG\Authenticate\Definition\UserDefinition; use ByJG\Authenticate\Exception\NotAuthenticatedException; use ByJG\Authenticate\Exception\UserExistsException; -use ByJG\Authenticate\UsersBase; +use ByJG\Authenticate\Service\UsersService; use ByJG\JwtWrapper\JwtHashHmacSecret; use ByJG\JwtWrapper\JwtWrapper; use PHPUnit\Framework\TestCase; @@ -13,20 +12,11 @@ abstract class TestUsersBase extends TestCase { /** - * @var UsersBase|null + * @var UsersService|null */ - protected UsersBase|null $object = null; - - /** - * @var UserDefinition - */ - protected $userDefinition; - - /** - * @var \ByJG\Authenticate\Definition\UserPropertiesDefinition - */ - protected $propertyDefinition; + protected UsersService|null $object = null; + protected string $loginField; protected $prefix = ""; @@ -35,16 +25,16 @@ abstract public function __setUp($loginField); public function __chooseValue($forUsername, $forEmail): string { $searchForList = [ - $this->userDefinition->getUsername() => $forUsername, - $this->userDefinition->getEmail() => $forEmail, + 'email' => $forEmail, + 'username' => $forUsername, ]; - return $searchForList[$this->userDefinition->loginField()]; + return $searchForList[$this->loginField]; } #[\Override] public function setUp(): void { - $this->__setUp(UserDefinition::LOGIN_IS_USERNAME); + $this->__setUp(UsersService::LOGIN_IS_USERNAME); } /** @@ -61,17 +51,17 @@ public function testAddUserError(): void public function testAddProperty(): void { // Check state - $user = $this->object->get($this->prefix . '2'); + $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->get($this->prefix . '2'); + $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->get($this->prefix . '2'); + $user = $this->object->getById($this->prefix . '2'); $this->assertEquals(['Rio de Janeiro', 'Belo Horizonte'], $user->get('city')); // Get Property @@ -79,12 +69,12 @@ public function testAddProperty(): void // Add another property $this->object->addProperty($this->prefix . '2', 'state', 'RJ'); - $user = $this->object->get($this->prefix . '2'); + $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->get($this->prefix . '2'); + $user = $this->object->getById($this->prefix . '2'); $this->assertEmpty($user->get('state')); // Remove Property Again @@ -99,32 +89,32 @@ public function testRemoveAllProperties(): void $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->get($this->prefix . '2'); + $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->get($this->prefix . '1'); + $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->get($this->prefix . '2'); + $user = $this->object->getById($this->prefix . '2'); $this->assertEquals(['Rio de Janeiro', 'Niteroi'], $user->get('city')); $this->assertEmpty($user->get('state')); - $user = $this->object->get($this->prefix . '1'); + $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->get($this->prefix . '2'); + $user = $this->object->getById($this->prefix . '2'); $this->assertEquals('Rio de Janeiro', $user->get('city')); $this->assertEmpty($user->get('state')); - $user = $this->object->get($this->prefix . '1'); + $user = $this->object->getById($this->prefix . '1'); $this->assertEmpty($user->get('city')); $this->assertEmpty($user->get('state')); @@ -134,13 +124,13 @@ public function testRemoveByLoginField(): void { $login = $this->__chooseValue('user1', 'user1@gmail.com'); - $user = $this->object->get($login, $this->object->getUserDefinition()->loginField()); + $user = $this->object->getByLogin($login); $this->assertNotNull($user); - $result = $this->object->removeByLoginField($login); + $result = $this->object->removeByLogin($login); $this->assertTrue($result); - $user = $this->object->get($login, $this->object->getUserDefinition()->loginField()); + $user = $this->object->getByLogin($login); $this->assertNull($user); } @@ -149,7 +139,7 @@ public function testEditUser(): void $login = $this->__chooseValue('user1', 'user1@gmail.com'); // Getting data - $user = $this->object->get($login, $this->object->getUserDefinition()->loginField()); + $user = $this->object->getByLogin($login); $this->assertEquals('User 1', $user->getName()); // Change and Persist data @@ -157,7 +147,7 @@ public function testEditUser(): void $this->object->save($user); // Check if data persists - $user = $this->object->get($this->prefix . '1'); + $user = $this->object->getById($this->prefix . '1'); $this->assertEquals('Other name', $user->getName()); } @@ -178,17 +168,17 @@ public function testIsValidUser(): void public function testIsAdmin(): void { // Check is Admin - $user3 = $this->object->get($this->prefix . '3'); + $user3 = $this->object->getById($this->prefix . '3'); $this->assertFalse($user3->isAdmin()); // Set the Admin Flag $login = $this->__chooseValue('user3', 'user3@gmail.com'); - $user = $this->object->get($login, $this->object->getUserDefinition()->loginField()); + $user = $this->object->getByLogin($login); $user->setAdmin('Y'); $this->object->save($user); // Check is Admin - $user3 = $this->object->get($this->prefix . '3'); + $user3 = $this->object->getById($this->prefix . '3'); $this->assertTrue($user3->isAdmin()); } @@ -207,7 +197,7 @@ protected function expectedToken($tokenData, $login, $userId): void ['tokenData'=>$tokenData] ); - $user = $this->object->get($login, $this->object->getUserDefinition()->loginField()); + $user = $this->object->getByLogin($login); $dataFromToken = new \stdClass(); $dataFromToken->tokenData = $tokenData; @@ -254,18 +244,18 @@ abstract public function testSaveAndSave(); public function testRemoveUserById(): void { - $user = $this->object->get($this->prefix . '1'); + $user = $this->object->getById($this->prefix . '1'); $this->assertNotNull($user); - $this->object->removeUserById($this->prefix . '1'); + $this->object->removeById($this->prefix . '1'); - $user2 = $this->object->get($this->prefix . '1'); + $user2 = $this->object->getById($this->prefix . '1'); $this->assertNull($user2); } public function testGetByUsername(): void { - $user = $this->object->get('user2', $this->object->getUserDefinition()->getUsername()); + $user = $this->object->getByUsername('user2'); $this->assertEquals($this->prefix . '2', $user->getUserid()); $this->assertEquals('User 2', $user->getName()); @@ -277,12 +267,12 @@ public function testGetByUsername(): void public function testGetByUserProperty(): void { // Add property to user1 - $user = $this->object->get($this->prefix . '1'); + $user = $this->object->getById($this->prefix . '1'); $user->set('property1', 'somevalue'); $this->object->save($user); // Add property to user2 - $user = $this->object->get($this->prefix . '2'); + $user = $this->object->getById($this->prefix . '2'); $user->set('property1', 'value1'); $user->set('property2', 'value2'); $this->object->save($user); @@ -315,4 +305,187 @@ public function testSetProperty(): void $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->withPasswordDefinition($passwordDef); + $user->setPassword('ValidPass8642'); // Valid: uppercase, lowercase, numbers, no sequential, 12 chars + + // 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->withPasswordDefinition($passwordDef); + $user->setPassword('StrongPass84!'); // Valid: uppercase, lowercase, 2 numbers, symbol, no sequential, 13 chars + + // Should update successfully + $savedUser = $this->object->save($user); + $this->assertNotNull($savedUser); + + // 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()); + } + + 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, + \ByJG\Authenticate\Service\UsersService::LOGIN_IS_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 + ); + } } diff --git a/tests/UsersDBDataset2ByEmailTest.php b/tests/UsersDBDataset2ByEmailTest.php index a97c5c3..5fb9809 100644 --- a/tests/UsersDBDataset2ByEmailTest.php +++ b/tests/UsersDBDataset2ByEmailTest.php @@ -2,13 +2,13 @@ namespace Tests; -use ByJG\Authenticate\Definition\UserDefinition; +use ByJG\Authenticate\Service\UsersService; class UsersDBDataset2ByEmailTest extends UsersDBDataset2ByUserNameTestUsersBase { #[\Override] public function setUp(): void { - $this->__setUp(UserDefinition::LOGIN_IS_EMAIL); + $this->__setUp(UsersService::LOGIN_IS_EMAIL); } } diff --git a/tests/UsersDBDataset2ByUserNameTestUsersBase.php b/tests/UsersDBDataset2ByUserNameTestUsersBase.php index 5480fda..a9feb9a 100644 --- a/tests/UsersDBDataset2ByUserNameTestUsersBase.php +++ b/tests/UsersDBDataset2ByUserNameTestUsersBase.php @@ -2,12 +2,12 @@ namespace Tests; +use ByJG\AnyDataset\Db\DatabaseExecutor; use ByJG\AnyDataset\Db\Factory; -use ByJG\Authenticate\Definition\UserDefinition; -use ByJG\Authenticate\Definition\UserPropertiesDefinition; -use ByJG\Authenticate\Model\UserModel; -use ByJG\Authenticate\UsersDBDataset; -use ByJG\MicroOrm\Exception\OrmModelInvalidException; +use ByJG\Authenticate\Repository\UserPropertiesRepository; +use ByJG\Authenticate\Repository\UsersRepository; +use ByJG\Authenticate\Service\UsersService; +use ByJG\Util\Uri; class UsersDBDataset2ByUserNameTestUsersBase extends TestUsersBase { @@ -15,55 +15,37 @@ class UsersDBDataset2ByUserNameTestUsersBase extends TestUsersBase protected $db; - /** - * @throws \ReflectionException - * @throws OrmModelInvalidException - */ #[\Override] public function __setUp($loginField) { $this->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), + 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), + 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 + $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'); @@ -74,17 +56,15 @@ public function __setUp($loginField) #[\Override] public function setUp(): void { - $this->__setUp(UserDefinition::LOGIN_IS_USERNAME); + $this->__setUp(UsersService::LOGIN_IS_USERNAME); } #[\Override] public function tearDown(): void { - $uri = new \ByJG\Util\Uri(self::CONNECTION_STRING); + $uri = new Uri(self::CONNECTION_STRING); unlink($uri->getPath()); $this->object = null; - $this->userDefinition = null; - $this->propertyDefinition = null; } /** @@ -97,7 +77,7 @@ public function testAddUser() $login = $this->__chooseValue('john', 'johndoe@gmail.com'); - $user = $this->object->get($login, $this->object->getUserDefinition()->loginField()); + $user = $this->object->getByLogin($login); $this->assertEquals('4', $user->getUserid()); $this->assertEquals('John Doe', $user->getName()); $this->assertEquals('john', $user->getUsername()); @@ -110,7 +90,7 @@ public function testAddUser() $user->setAdmin('y'); $this->object->save($user); - $user2 = $this->object->get($login, $this->object->getUserDefinition()->loginField()); + $user2 = $this->object->getByLogin($login); $this->assertEquals('y', $user2->getAdmin()); } @@ -130,10 +110,10 @@ public function testCreateAuthToken() #[\Override] public function testSaveAndSave() { - $user = $this->object->get("1"); + $user = $this->object->getById("1"); $this->object->save($user); - $user2 = $this->object->get("1"); + $user2 = $this->object->getById("1"); $this->assertEquals($user, $user2); } diff --git a/tests/UsersDBDatasetByEmailTest.php b/tests/UsersDBDatasetByEmailTest.php index 83a3f0a..e49d400 100644 --- a/tests/UsersDBDatasetByEmailTest.php +++ b/tests/UsersDBDatasetByEmailTest.php @@ -2,13 +2,13 @@ namespace Tests; -use ByJG\Authenticate\Definition\UserDefinition; +use ByJG\Authenticate\Service\UsersService; class UsersDBDatasetByEmailTest extends UsersDBDatasetByUsernameTestUsersBase { #[\Override] public function setUp(): void { - $this->__setUp(UserDefinition::LOGIN_IS_EMAIL); + $this->__setUp(UsersService::LOGIN_IS_EMAIL); } } diff --git a/tests/UsersDBDatasetByUsernameTestUsersBase.php b/tests/UsersDBDatasetByUsernameTestUsersBase.php index df015df..e8d5647 100644 --- a/tests/UsersDBDatasetByUsernameTestUsersBase.php +++ b/tests/UsersDBDatasetByUsernameTestUsersBase.php @@ -2,12 +2,13 @@ namespace Tests; +use ByJG\AnyDataset\Db\DatabaseExecutor; use ByJG\AnyDataset\Db\Factory; -use ByJG\Authenticate\Definition\UserDefinition; -use ByJG\Authenticate\Definition\UserPropertiesDefinition; -use ByJG\Authenticate\MapperFunctions\ClosureMapper; use ByJG\Authenticate\Model\UserModel; -use ByJG\Authenticate\UsersDBDataset; +use ByJG\Authenticate\Model\UserPropertiesModel; +use ByJG\Authenticate\Repository\UserPropertiesRepository; +use ByJG\Authenticate\Repository\UsersRepository; +use ByJG\Authenticate\Service\UsersService; use ByJG\Util\Uri; class UsersDBDatasetByUsernameTestUsersBase extends TestUsersBase @@ -21,32 +22,33 @@ class UsersDBDatasetByUsernameTestUsersBase extends TestUsersBase public function __setUp($loginField) { $this->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 datetime default (datetime(\'2017-12-04\')), + 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), + 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 + $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'); @@ -56,7 +58,7 @@ public function __setUp($loginField) $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'); } @@ -64,7 +66,7 @@ public function __setUp($loginField) #[\Override] public function setUp(): void { - $this->__setUp(UserDefinition::LOGIN_IS_USERNAME); + $this->__setUp(UsersService::LOGIN_IS_USERNAME); } #[\Override] @@ -73,8 +75,6 @@ public function tearDown(): void $uri = new Uri(self::CONNECTION_STRING); unlink($uri->getPath()); $this->object = null; - $this->userDefinition = null; - $this->propertyDefinition = null; } /** @@ -87,7 +87,7 @@ public function testAddUser() $login = $this->__chooseValue('john', 'johndoe@gmail.com'); - $user = $this->object->get($login, $this->object->getUserDefinition()->loginField()); + $user = $this->object->getByLogin($login); $this->assertEquals('4', $user->getUserid()); $this->assertEquals('John Doe', $user->getName()); $this->assertEquals('john', $user->getUsername()); @@ -100,7 +100,7 @@ public function testAddUser() $user->setAdmin('y'); $this->object->save($user); - $user2 = $this->object->get($login, $this->object->getUserDefinition()->loginField()); + $user2 = $this->object->getByLogin($login); $this->assertEquals('y', $user2->getAdmin()); } @@ -117,56 +117,18 @@ public function testCreateAuthToken() /** * @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() { - // For Update Definitions - $this->userDefinition->defineMapperForUpdate(UserDefinition::FIELD_NAME, new ClosureMapper(function ($value, $instance) { - return '[' . $value . ']'; - })); - $this->userDefinition->defineMapperForUpdate(UserDefinition::FIELD_USERNAME, new ClosureMapper(function ($value, $instance) { - return ']' . $value . '['; - })); - $this->userDefinition->defineMapperForUpdate(UserDefinition::FIELD_EMAIL, new ClosureMapper(function ($value, $instance) { - return '-' . $value . '-'; - })); - $this->userDefinition->defineMapperForUpdate(UserDefinition::FIELD_PASSWORD, new ClosureMapper(function ($value, $instance) { - return "@" . $value . "@"; - })); - $this->userDefinition->markPropertyAsReadOnly(UserDefinition::FIELD_CREATED); - - // For Select Definitions - $this->userDefinition->defineMapperForSelect(UserDefinition::FIELD_NAME, new ClosureMapper(function ($value, $instance) { - return '(' . $value . ')'; - })); - $this->userDefinition->defineMapperForSelect(UserDefinition::FIELD_USERNAME, new ClosureMapper(function ($value, $instance) { - return ')' . $value . '('; - })); - $this->userDefinition->defineMapperForSelect(UserDefinition::FIELD_EMAIL, new ClosureMapper(function ($value, $instance) { - return '#' . $value . '#'; - })); - $this->userDefinition->defineMapperForSelect(UserDefinition::FIELD_PASSWORD, new ClosureMapper(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->get($login, $newObject->getUserDefinition()->loginField()); - $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()); + // 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 @@ -174,10 +136,10 @@ public function testWithUpdateValue() #[\Override] public function testSaveAndSave() { - $user = $this->object->get("1"); + $user = $this->object->getById("1"); $this->object->save($user); - $user2 = $this->object->get("1"); + $user2 = $this->object->getById("1"); $this->assertEquals($user, $user2); } diff --git a/tests/UsersDBDatasetDefinitionTest.php b/tests/UsersDBDatasetDefinitionTest.php index 187dafe..b0541a8 100644 --- a/tests/UsersDBDatasetDefinitionTest.php +++ b/tests/UsersDBDatasetDefinitionTest.php @@ -3,21 +3,20 @@ 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\Exception\UserExistsException; -use ByJG\Authenticate\MapperFunctions\ClosureMapper; -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\TestUniqueIdGenerator; +use Tests\Fixture\MyUserPropertiesModel; class UsersDBDatasetDefinitionTest extends UsersDBDatasetByUsernameTestUsersBase { @@ -35,48 +34,34 @@ class UsersDBDatasetDefinitionTest extends UsersDBDatasetByUsernameTestUsersBase 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), + 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));' ); $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( @@ -100,7 +85,15 @@ public function __setUp($loginField) #[Override] public function setUp(): void { - $this->__setUp(UserDefinition::LOGIN_IS_USERNAME); + $this->__setUp(UsersService::LOGIN_IS_USERNAME); + } + + #[\Override] + public function tearDown(): void + { + $uri = new Uri(self::CONNECTION_STRING); + unlink($uri->getPath()); + $this->object = null; } /** @@ -117,7 +110,7 @@ public function testAddUser() $login = $this->__chooseValue('john', 'johndoe@gmail.com'); - $user = $this->object->get($login, $this->object->getUserDefinition()->loginField()); + $user = $this->object->getByLogin($login); $this->assertEquals('4', $user->getUserid()); $this->assertEquals('John Doe', $user->getName()); $this->assertEquals('john', $user->getUsername()); @@ -126,161 +119,45 @@ public function testAddUser() $this->assertEquals('no', $user->getAdmin()); /** @psalm-suppress UndefinedMethod Check UserModel::__call */ $this->assertEquals('other john', $user->getOtherfield()); - $this->assertEquals('', $user->getCreated()); // There is no default action for it + $this->assertEquals('2017-12-04 00:00:00', $user->getCreated()); // Database default value // Setting as Admin $user->setAdmin('y'); $this->object->save($user); - $user2 = $this->object->get($login, $this->object->getUserDefinition()->loginField()); + $user2 = $this->object->getByLogin($login); $this->assertEquals('y', $user2->getAdmin()); } - /** - * @throws Exception - * - * @return void - */ - #[Override] + // 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->defineMapperForUpdate(UserDefinition::FIELD_NAME, new ClosureMapper(function ($value, $instance) { - return '[' . $value . ']'; - })); - $this->userDefinition->defineMapperForUpdate(UserDefinition::FIELD_USERNAME, new ClosureMapper(function ($value, $instance) { - return ']' . $value . '['; - })); - $this->userDefinition->defineMapperForUpdate(UserDefinition::FIELD_EMAIL, new ClosureMapper(function ($value, $instance) { - return '-' . $value . '-'; - })); - $this->userDefinition->defineMapperForUpdate(UserDefinition::FIELD_PASSWORD, new ClosureMapper(function ($value, $instance) { - return "@" . $value . "@"; - })); - $this->userDefinition->markPropertyAsReadOnly(UserDefinition::FIELD_CREATED); - $this->userDefinition->defineMapperForUpdate('otherfield', new ClosureMapper(function ($value, $instance) { - return "*" . $value . "*"; - })); - - // For Select Definitions - $this->userDefinition->defineMapperForSelect(UserDefinition::FIELD_NAME, new ClosureMapper(function ($value, $instance) { - return '(' . $value . ')'; - })); - $this->userDefinition->defineMapperForSelect(UserDefinition::FIELD_USERNAME, new ClosureMapper(function ($value, $instance) { - return ')' . $value . '('; - })); - $this->userDefinition->defineMapperForSelect(UserDefinition::FIELD_EMAIL, new ClosureMapper(function ($value, $instance) { - return '#' . $value . '#'; - })); - $this->userDefinition->defineMapperForSelect(UserDefinition::FIELD_PASSWORD, new ClosureMapper(function ($value, $instance) { - return '%' . $value . '%'; - })); - $this->userDefinition->defineMapperForSelect('otherfield', new ClosureMapper(function ($value, $instance) { - return ']' . $value . '['; - })); - - // Test it! - $newObject = new UsersDBDataset( - $this->db, - $this->userDefinition, - $this->propertyDefinition - ); - - $newObject->save( - new MyUserModel('User 4', 'user4@gmail.com', 'user4', 'pwd4', 'no', 'other john') - ); - - $login = $this->__chooseValue('user4', 'user4@gmail.com'); - - $user = $newObject->get($login, $newObject->getUserDefinition()->loginField()); - $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()); + // 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. } - /** - * @throws Exception - */ - public function testDefineGenerateKeyWithInterface(): void + public function testDefineGenerateKeyWithInterface() { - // Create a separate table with varchar userid for testing custom generators - $this->db->execute('create table users_custom ( - userid varchar(50) primary key, - name varchar(45), - email varchar(200), - username varchar(20), - password varchar(40), - created datetime default (datetime(\'2017-12-04\')), - admin char(1));' - ); - - // Create a new user definition with custom generator - $userDefinition = new UserDefinition('users_custom', UserModel::class, UserDefinition::LOGIN_IS_USERNAME); - $generator = new TestUniqueIdGenerator('CUSTOM-'); - $userDefinition->defineGenerateKey($generator); - - // Create dataset with custom definition - $dataset = new UsersDBDataset($this->db, $userDefinition, $this->propertyDefinition); - - // Add a user - the custom generator should be used - $user = $dataset->addUser('Test User', 'testuser', 'test@example.com', 'password123'); - - // Verify the user ID was generated with the custom prefix - $this->assertStringStartsWith('CUSTOM-', (string)$user->getUserid()); - $this->assertEquals('Test User', $user->getName()); - $this->assertEquals('testuser', $user->getUsername()); - - // Cleanup - $this->db->execute('drop table users_custom'); + // 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. } - /** - * @throws Exception - */ - public function testDefineGenerateKeyWithString(): void + public function testDefineGenerateKeyWithString() { - // Create a separate table with varchar userid for testing custom generators - $this->db->execute('create table users_custom2 ( - userid varchar(50) primary key, - name varchar(45), - email varchar(200), - username varchar(20), - password varchar(40), - created datetime default (datetime(\'2017-12-04\')), - admin char(1));' - ); - - // Create a new user definition with generator class string - $userDefinition = new UserDefinition('users_custom2', UserModel::class, UserDefinition::LOGIN_IS_USERNAME); - $userDefinition->defineGenerateKey(TestUniqueIdGenerator::class); - - // Create dataset with custom definition - $dataset = new UsersDBDataset($this->db, $userDefinition, $this->propertyDefinition); - - // Add a user - the custom generator should be instantiated and used - $user = $dataset->addUser('Test User 2', 'testuser2', 'test2@example.com', 'password123'); - - // Verify the user ID was generated with the default TEST- prefix - $this->assertStringStartsWith('TEST-', (string)$user->getUserid()); - $this->assertEquals('Test User 2', $user->getName()); - - // Cleanup - $this->db->execute('drop table users_custom2'); + // 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. } - public function testDefineGenerateKeyClosureThrowsException(): void + public function testDefineGenerateKeyClosureThrowsException() { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('defineGenerateKeyClosure is deprecated. Use defineGenerateKey with MapperFunctionsInterface instead.'); - - $userDefinition = new UserDefinition(); - $userDefinition->defineGenerateKeyClosure(function ($executor, $instance) { - return 'test-id'; - }); + // UserDefinition has been removed in the new architecture. } + */ } From 709fb6057cbbe595d9b6595fa17df23d6ba9955e Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Sat, 15 Nov 2025 16:50:43 -0500 Subject: [PATCH 21/40] Refactor user role management: replace `admin` field with `role` field, update related methods (`isAdmin` -> `hasRole`), constructors, tests, and documentation to support flexible role definitions and case-insensitive role checks. --- docs/custom-fields.md | 2 +- docs/database-storage.md | 2 +- docs/examples.md | 4 +-- docs/mappers.md | 4 +-- docs/user-management.md | 29 +++++++++++-------- src/Model/UserModel.php | 29 +++++++++++++------ src/Service/UsersService.php | 4 --- tests/CustomUserModel.php | 4 +-- tests/Fixture/MyUserModel.php | 8 ++--- tests/Fixture/UserModelMd5.php | 2 +- tests/PasswordMd5MapperTest.php | 6 ++-- tests/TestUsersBase.php | 25 ++++++++++++---- ...UsersDBDataset2ByUserNameTestUsersBase.php | 10 +++---- .../UsersDBDatasetByUsernameTestUsersBase.php | 12 ++++---- tests/UsersDBDatasetDefinitionTest.php | 18 ++++++------ 15 files changed, 92 insertions(+), 67 deletions(-) diff --git a/docs/custom-fields.md b/docs/custom-fields.md index 2900c90..3682b7f 100644 --- a/docs/custom-fields.md +++ b/docs/custom-fields.md @@ -105,7 +105,7 @@ CREATE TABLE users username VARCHAR(15) NOT NULL, password CHAR(40) NOT NULL, created DATETIME, - admin ENUM('Y','N'), + role VARCHAR(20), -- Custom fields phone VARCHAR(20), department VARCHAR(50), diff --git a/docs/database-storage.md b/docs/database-storage.md index 8f69ffd..d655dca 100644 --- a/docs/database-storage.md +++ b/docs/database-storage.md @@ -22,7 +22,7 @@ CREATE TABLE users username VARCHAR(15) NOT NULL, password CHAR(40) NOT NULL, created DATETIME, - admin ENUM('Y','N'), + role VARCHAR(20), CONSTRAINT pk_users PRIMARY KEY (userid) ) ENGINE=InnoDB; diff --git a/docs/examples.md b/docs/examples.md index 6a87e88..77a1e0e 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -227,7 +227,7 @@ $loginTime = $sessionContext->getSessionData('login_time');

Email: getEmail()) ?>

Logged in at:

- isAdmin()): ?> + hasRole('admin')): ?>

You are an administrator

Admin Panel

@@ -383,7 +383,7 @@ try { 'name' => $user->getName(), 'email' => $user->getEmail(), 'username' => $user->getUsername(), - 'admin' => $user->isAdmin() + 'role' => $user->getRole() ]); } elseif ($_SERVER['REQUEST_METHOD'] === 'PUT') { // Update user info diff --git a/docs/mappers.md b/docs/mappers.md index 26e693d..bc2917c 100644 --- a/docs/mappers.md +++ b/docs/mappers.md @@ -395,8 +395,8 @@ class DefaultsProcessor implements EntityProcessorInterface if (empty($instance->getCreated())) { $instance->setCreated(date('Y-m-d H:i:s')); } - if (empty($instance->getAdmin())) { - $instance->setAdmin('no'); + if (empty($instance->getRole())) { + $instance->setRole('user'); } } } diff --git a/docs/user-management.md b/docs/user-management.md index 7e3e49f..9a94d2b 100644 --- a/docs/user-management.md +++ b/docs/user-management.md @@ -34,7 +34,7 @@ $userModel->setName('John Doe'); $userModel->setUsername('johndoe'); $userModel->setEmail('john@example.com'); $userModel->setPassword('SecurePass123'); -$userModel->setAdmin('no'); +$userModel->setRole('user'); $savedUser = $users->save($userModel); ``` @@ -124,25 +124,30 @@ $users->removeById($userId); $users->removeByLogin('johndoe'); ``` -## Checking Admin Status +## Checking User Roles -The admin flag is now interpreted entirely inside `UserModel`. Use `$user->isAdmin()` to read the computed boolean value, -and `$user->setAdmin(true)` (or one of the accepted string values) to change it. This replaces the old `$users->isAdmin()` -method that lived in `UsersInterface`. +Users can have assigned roles stored in the `role` field. Use `$user->hasRole()` to check if a user has a specific role: ```php isAdmin()) { +if ($user->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 admin field accepts the following values as `true`: -- `yes`, `YES`, `y`, `Y` -- `true`, `TRUE`, `t`, `T` -- `1` -- `s`, `S` (from Portuguese "sim") +The `hasRole()` method performs case-insensitive comparison, so `hasRole('admin')` and `hasRole('ADMIN')` are equivalent. ## UserModel Properties @@ -156,7 +161,7 @@ The `UserModel` class provides the following properties: | username | string\|null | User's username | | password | string\|null | User's password (hashed) | | created | string\|null | Creation timestamp | -| admin | string\|null | Admin flag (yes/no) | +| role | string\|null | User's role (admin, moderator, user, etc.) | ## Next Steps diff --git a/src/Model/UserModel.php b/src/Model/UserModel.php index 96c70ed..6ae0be7 100644 --- a/src/Model/UserModel.php +++ b/src/Model/UserModel.php @@ -32,7 +32,7 @@ class UserModel protected ?string $created = null; #[FieldAttribute] - protected ?string $admin = null; + protected ?string $role = null; protected ?PasswordDefinition $passwordDefinition = null; @@ -45,15 +45,15 @@ 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; } @@ -164,17 +164,28 @@ public function setCreated(?string $created): void /** * @return string|null */ - public function getAdmin(): ?string + public function getRole(): ?string { - return $this->admin; + return $this->role; } /** - * @param string|null $admin + * @param string|null $role */ - public function setAdmin(?string $admin): void + public function setRole(?string $role): void { - $this->admin = $admin; + $this->role = $role; + } + + /** + * Check if user has a specific role + * + * @param string $role Role name to check + * @return bool + */ + public function hasRole(string $role): bool + { + return $this->role !== null && $this->role !== '' && strcasecmp($this->role, $role) === 0; } public function set(string $name, string|null $value): void diff --git a/src/Service/UsersService.php b/src/Service/UsersService.php index 22c89e6..ae5df61 100644 --- a/src/Service/UsersService.php +++ b/src/Service/UsersService.php @@ -249,10 +249,6 @@ public function hasProperty(string|int|HexUuidLiteral $userId, string $propertyN return false; } - if ($user->isAdmin()) { - return true; - } - $values = $user->get($propertyName); if ($values === null) { diff --git a/tests/CustomUserModel.php b/tests/CustomUserModel.php index 3137397..8815a84 100644 --- a/tests/CustomUserModel.php +++ b/tests/CustomUserModel.php @@ -29,6 +29,6 @@ class CustomUserModel extends UserModel #[FieldAttribute(fieldName: 'mycreated', updateFunction: ReadOnlyMapper::class)] protected ?string $created = null; - #[FieldAttribute(fieldName: 'myadmin')] - protected ?string $admin = null; + #[FieldAttribute(fieldName: 'myrole')] + protected ?string $role = null; } diff --git a/tests/Fixture/MyUserModel.php b/tests/Fixture/MyUserModel.php index 183f932..bc79e38 100644 --- a/tests/Fixture/MyUserModel.php +++ b/tests/Fixture/MyUserModel.php @@ -30,15 +30,15 @@ class MyUserModel extends UserModel #[FieldAttribute(fieldName: 'mycreated', updateFunction: ReadOnlyMapper::class)] protected ?string $created = null; - #[FieldAttribute(fieldName: 'myadmin')] - protected ?string $admin = null; + #[FieldAttribute(fieldName: 'myrole')] + protected ?string $role = null; #[FieldAttribute(fieldName: 'myotherfield')] protected $otherfield; - public function __construct($name = "", $email = "", $username = "", $password = "", $admin = "no", $field = "") + public function __construct($name = "", $email = "", $username = "", $password = "", $role = "", $field = "") { - parent::__construct($name, $email, $username, $password, $admin); + parent::__construct($name, $email, $username, $password, $role); $this->setOtherfield($field); } diff --git a/tests/Fixture/UserModelMd5.php b/tests/Fixture/UserModelMd5.php index 84c96fd..dfdd702 100644 --- a/tests/Fixture/UserModelMd5.php +++ b/tests/Fixture/UserModelMd5.php @@ -29,5 +29,5 @@ class UserModelMd5 extends UserModel protected ?string $created = null; #[FieldAttribute] - protected ?string $admin = null; + protected ?string $role = null; } diff --git a/tests/PasswordMd5MapperTest.php b/tests/PasswordMd5MapperTest.php index c870e49..1c84eec 100644 --- a/tests/PasswordMd5MapperTest.php +++ b/tests/PasswordMd5MapperTest.php @@ -31,7 +31,7 @@ public function setUp(): void username varchar(20), password varchar(40), created datetime default (datetime(\'2017-12-04\')), - admin char(1));' + role varchar(20));' ); $this->db->execute('create table users_property ( @@ -127,7 +127,7 @@ public function testPasswordRemainsUnchangedWhenUpdatingOtherFields(): void // Update other fields WITHOUT touching the password $user->setName('John Updated'); $user->setEmail('johnupdated@example.com'); - $user->setAdmin('y'); + $user->setRole('admin'); $updatedUser = $this->service->save($user); // Verify the password hash remained exactly the same @@ -137,7 +137,7 @@ public function testPasswordRemainsUnchangedWhenUpdatingOtherFields(): void // Verify other fields were updated $this->assertEquals('John Updated', $updatedUser->getName()); $this->assertEquals('johnupdated@example.com', $updatedUser->getEmail()); - $this->assertEquals('y', $updatedUser->getAdmin()); + $this->assertEquals('admin', $updatedUser->getRole()); // Verify user can still login with original password $authenticatedUser = $this->service->isValidUser('johnsmith', $originalPassword); diff --git a/tests/TestUsersBase.php b/tests/TestUsersBase.php index 535f82d..9dda3f6 100644 --- a/tests/TestUsersBase.php +++ b/tests/TestUsersBase.php @@ -167,19 +167,32 @@ public function testIsValidUser(): void public function testIsAdmin(): void { - // Check is Admin + // Check user has no role initially $user3 = $this->object->getById($this->prefix . '3'); - $this->assertFalse($user3->isAdmin()); + $this->assertFalse($user3->hasRole('admin')); + $this->assertEmpty($user3->getRole()); - // Set the Admin Flag + // Set a role $login = $this->__chooseValue('user3', 'user3@gmail.com'); $user = $this->object->getByLogin($login); - $user->setAdmin('Y'); + $user->setRole('admin'); $this->object->save($user); - // Check is Admin + // Check user has the role $user3 = $this->object->getById($this->prefix . '3'); - $this->assertTrue($user3->isAdmin()); + $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($tokenData, $login, $userId): void diff --git a/tests/UsersDBDataset2ByUserNameTestUsersBase.php b/tests/UsersDBDataset2ByUserNameTestUsersBase.php index a9feb9a..2b8e8a0 100644 --- a/tests/UsersDBDataset2ByUserNameTestUsersBase.php +++ b/tests/UsersDBDataset2ByUserNameTestUsersBase.php @@ -29,7 +29,7 @@ public function __setUp($loginField) myusername varchar(20), mypassword varchar(40), mycreated datetime default (datetime(\'2017-12-04\')), - myadmin char(1));' + myrole varchar(20));' ); $this->db->execute('create table theirproperty ( @@ -83,15 +83,15 @@ public function testAddUser() $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()); $this->assertEquals('2017-12-04 00:00:00', $user->getCreated()); - // Setting as Admin - $user->setAdmin('y'); + // Setting role + $user->setRole('admin'); $this->object->save($user); $user2 = $this->object->getByLogin($login); - $this->assertEquals('y', $user2->getAdmin()); + $this->assertEquals('admin', $user2->getRole()); } /** diff --git a/tests/UsersDBDatasetByUsernameTestUsersBase.php b/tests/UsersDBDatasetByUsernameTestUsersBase.php index e8d5647..68686b7 100644 --- a/tests/UsersDBDatasetByUsernameTestUsersBase.php +++ b/tests/UsersDBDatasetByUsernameTestUsersBase.php @@ -32,7 +32,7 @@ public function __setUp($loginField) username varchar(20), password varchar(40), created datetime default (datetime(\'2017-12-04\')), - admin char(1));' + role varchar(20));' ); $this->db->execute('create table users_property ( @@ -56,7 +56,7 @@ public function __setUp($loginField) $this->assertEquals('User 1', $user->getName()); $this->assertEquals('user1', $user->getUsername()); $this->assertEquals('a63d4b132a9a1d3430f9ae507825f572449e0d17', $user->getPassword()); - $this->assertEquals('no', $user->getAdmin()); + $this->assertEquals('', $user->getRole()); $this->assertEquals('2017-12-04 00:00:00', $user->getCreated()); $this->object->addUser('User 2', 'user2', 'user2@gmail.com', 'pwd2'); @@ -93,15 +93,15 @@ public function testAddUser() $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()); $this->assertEquals('2017-12-04 00:00:00', $user->getCreated()); - // Setting as Admin - $user->setAdmin('y'); + // Setting role + $user->setRole('admin'); $this->object->save($user); $user2 = $this->object->getByLogin($login); - $this->assertEquals('y', $user2->getAdmin()); + $this->assertEquals('admin', $user2->getRole()); } /** diff --git a/tests/UsersDBDatasetDefinitionTest.php b/tests/UsersDBDatasetDefinitionTest.php index b0541a8..3ba1928 100644 --- a/tests/UsersDBDatasetDefinitionTest.php +++ b/tests/UsersDBDatasetDefinitionTest.php @@ -45,7 +45,7 @@ public function __setUp($loginField) mypassword varchar(40), myotherfield varchar(40), mycreated datetime default (datetime(\'2017-12-04\')), - myadmin char(1));' + myrole varchar(20));' ); $this->db->execute('create table theirproperty ( @@ -65,13 +65,13 @@ public function __setUp($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', '', 'other 3') ); } @@ -106,7 +106,7 @@ public function tearDown(): 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'); @@ -116,17 +116,17 @@ public function testAddUser() $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('2017-12-04 00:00:00', $user->getCreated()); // Database default value - // Setting as Admin - $user->setAdmin('y'); + // Setting role + $user->setRole('admin'); $this->object->save($user); $user2 = $this->object->getByLogin($login); - $this->assertEquals('y', $user2->getAdmin()); + $this->assertEquals('admin', $user2->getRole()); } // TODO: These tests are currently disabled because the new architecture uses From 4e2b4f839258134dbc5c76058e9cfbd4ea5b0453 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Sat, 15 Nov 2025 17:09:49 -0500 Subject: [PATCH 22/40] Add timestamp fields to `UserModel`: replace `created` field with `createdAt`, introduce `updatedAt` and `deletedAt` fields with proper mappers, and update tests and documentation accordingly. --- docs/custom-fields.md | 12 ++-- docs/database-storage.md | 18 +++-- docs/mappers.md | 8 +-- src/Model/UserModel.php | 34 ++------- src/Service/UsersService.php | 5 ++ src/Service/UsersServiceInterface.php | 2 + tests/CustomUserModel.php | 11 ++- tests/Fixture/MyUserModel.php | 11 ++- tests/Fixture/UserModelMd5.php | 11 ++- tests/PasswordMd5MapperTest.php | 4 +- tests/TestUsersBase.php | 70 ++++++++++++++++--- ...UsersDBDataset2ByUserNameTestUsersBase.php | 17 ++++- .../UsersDBDatasetByUsernameTestUsersBase.php | 20 ++++-- tests/UsersDBDatasetDefinitionTest.php | 7 +- 14 files changed, 164 insertions(+), 66 deletions(-) diff --git a/docs/custom-fields.md b/docs/custom-fields.md index 3682b7f..39e49ab 100644 --- a/docs/custom-fields.md +++ b/docs/custom-fields.md @@ -104,7 +104,9 @@ CREATE TABLE users email VARCHAR(120), username VARCHAR(15) NOT NULL, password CHAR(40) NOT NULL, - created DATETIME, + created_at DATETIME, + updated_at DATETIME, + deleted_at DATETIME, role VARCHAR(20), -- Custom fields phone VARCHAR(20), @@ -189,8 +191,8 @@ use ByJG\MicroOrm\Attributes\FieldAttribute; class CustomUserModel extends UserModel { // Read-only field - can be set on creation but not updated - #[FieldAttribute(fieldName: 'created', updateFunction: ReadOnlyMapper::class)] - protected ?string $created = null; + #[FieldAttribute(fieldName: 'created_at', updateFunction: ReadOnlyMapper::class, insertFunction: NowUtcMapper::class)] + protected ?string $createdAt = null; // Read-only custom field #[FieldAttribute(fieldName: 'phone', updateFunction: ReadOnlyMapper::class)] @@ -237,10 +239,10 @@ use ByJG\MicroOrm\Attributes\FieldAttribute; class CustomUserModel extends UserModel { #[FieldAttribute( - fieldName: 'created', + fieldName: 'created_at', selectFunction: [FieldHandler::class, 'toDate'] )] - protected ?\DateTime $created = null; + protected ?\DateTime $createdAt = null; } ``` diff --git a/docs/database-storage.md b/docs/database-storage.md index d655dca..70cdc74 100644 --- a/docs/database-storage.md +++ b/docs/database-storage.md @@ -21,7 +21,9 @@ CREATE TABLE users email VARCHAR(120), username VARCHAR(15) NOT NULL, password CHAR(40) NOT NULL, - created DATETIME, + created_at DATETIME, + updated_at DATETIME, + deleted_at DATETIME, role VARCHAR(20), CONSTRAINT pk_users PRIMARY KEY (userid) @@ -108,11 +110,17 @@ class CustomUserModel extends UserModel #[FieldAttribute(fieldName: 'password_hash', updateFunction: PasswordSha1Mapper::class)] protected ?string $password = null; - #[FieldAttribute(fieldName: 'date_created', updateFunction: ReadOnlyMapper::class)] - protected ?string $created = null; + #[FieldAttribute(fieldName: 'date_created', updateFunction: ReadOnlyMapper::class, insertFunction: NowUtcMapper::class)] + protected ?string $createdAt = null; - #[FieldAttribute(fieldName: 'is_admin')] - protected ?string $admin = null; + #[FieldAttribute(fieldName: 'date_updated', updateFunction: NowUtcMapper::class)] + protected ?string $updatedAt = null; + + #[FieldAttribute(fieldName: 'date_deleted', syncWithDb: false)] + protected ?string $deletedAt = null; + + #[FieldAttribute(fieldName: 'user_role')] + protected ?string $role = null; } // Use custom model diff --git a/docs/mappers.md b/docs/mappers.md index bc2917c..5509eea 100644 --- a/docs/mappers.md +++ b/docs/mappers.md @@ -261,8 +261,8 @@ class CreatedTimestampProcessor implements EntityProcessorInterface public function process(mixed $instance): void { if ($instance instanceof UserModel) { - if (empty($instance->getCreated())) { - $instance->setCreated(date('Y-m-d H:i:s')); + if (empty($instance->getCreatedAt())) { + $instance->setCreatedAt(date('Y-m-d H:i:s')); } } } @@ -392,8 +392,8 @@ class DefaultsProcessor implements EntityProcessorInterface public function process(mixed $instance): void { if ($instance instanceof UserModel) { - if (empty($instance->getCreated())) { - $instance->setCreated(date('Y-m-d H:i:s')); + if (empty($instance->getCreatedAt())) { + $instance->setCreatedAt(date('Y-m-d H:i:s')); } if (empty($instance->getRole())) { $instance->setRole('user'); diff --git a/src/Model/UserModel.php b/src/Model/UserModel.php index 6ae0be7..d9f9544 100644 --- a/src/Model/UserModel.php +++ b/src/Model/UserModel.php @@ -7,12 +7,17 @@ use ByJG\MicroOrm\Attributes\FieldAttribute; use ByJG\MicroOrm\Attributes\TableAttribute; use ByJG\MicroOrm\Literal\HexUuidLiteral; -use ByJG\MicroOrm\MapperFunctions\ReadOnlyMapper; +use ByJG\MicroOrm\Trait\CreatedAt; +use ByJG\MicroOrm\Trait\DeletedAt; +use ByJG\MicroOrm\Trait\UpdatedAt; use InvalidArgumentException; #[TableAttribute(tableName: 'users')] class UserModel { + use CreatedAt; + use UpdatedAt; + use DeletedAt; #[FieldAttribute(primaryKey: true)] protected string|int|HexUuidLiteral|null $userid = null; @@ -28,9 +33,6 @@ class UserModel #[FieldAttribute(updateFunction: PasswordSha1Mapper::class)] protected ?string $password = null; - #[FieldAttribute(updateFunction: ReadOnlyMapper::class)] - protected ?string $created = null; - #[FieldAttribute] protected ?string $role = null; @@ -145,22 +147,6 @@ public function setPassword(?string $password): void $this->password = $password; } - /** - * @return string|null - */ - public function getCreated(): ?string - { - return $this->created; - } - - /** - * @param string|null $created - */ - public function setCreated(?string $created): void - { - $this->created = $created; - } - /** * @return string|null */ @@ -251,13 +237,7 @@ public function addProperty(UserPropertiesModel $property): void public function withPasswordDefinition(PasswordDefinition $passwordDefinition): static { $this->passwordDefinition = $passwordDefinition; + $this->setPassword($this->password); return $this; } - - public function isAdmin(): bool - { - return - preg_match('/^(yes|YES|[yY]|true|TRUE|[tT]|1|[sS])$/', $this->getAdmin()) === 1 - ; - } } diff --git a/src/Service/UsersService.php b/src/Service/UsersService.php index ae5df61..da4102f 100644 --- a/src/Service/UsersService.php +++ b/src/Service/UsersService.php @@ -450,4 +450,9 @@ public function isValidToken(string $login, JwtWrapper $jwtWrapper, string $toke 'data' => $data->data ]; } + + public function getUsersEntity(array $fields): UserModel + { + return $this->getUsersRepository()->getMapper()->getEntity($fields); + } } diff --git a/src/Service/UsersServiceInterface.php b/src/Service/UsersServiceInterface.php index 5c339dc..085080f 100644 --- a/src/Service/UsersServiceInterface.php +++ b/src/Service/UsersServiceInterface.php @@ -192,4 +192,6 @@ public function createAuthToken( * @return array|null */ public function isValidToken(string $login, JwtWrapper $jwtWrapper, string $token): ?array; + + public function getUsersEntity(array $fields): UserModel; } diff --git a/tests/CustomUserModel.php b/tests/CustomUserModel.php index 8815a84..2cf8cf2 100644 --- a/tests/CustomUserModel.php +++ b/tests/CustomUserModel.php @@ -6,6 +6,7 @@ use ByJG\Authenticate\Model\UserModel; use ByJG\MicroOrm\Attributes\FieldAttribute; use ByJG\MicroOrm\Attributes\TableAttribute; +use ByJG\MicroOrm\MapperFunctions\NowUtcMapper; use ByJG\MicroOrm\MapperFunctions\ReadOnlyMapper; #[TableAttribute(tableName: 'mytable')] @@ -26,8 +27,14 @@ class CustomUserModel extends UserModel #[FieldAttribute(fieldName: 'mypassword', updateFunction: PasswordSha1Mapper::class)] protected ?string $password = null; - #[FieldAttribute(fieldName: 'mycreated', updateFunction: ReadOnlyMapper::class)] - protected ?string $created = null; + #[FieldAttribute(fieldName: 'mycreated_at', updateFunction: ReadOnlyMapper::class, insertFunction: NowUtcMapper::class)] + protected ?string $createdAt = null; + + #[FieldAttribute(fieldName: 'myupdated_at', updateFunction: NowUtcMapper::class)] + protected ?string $updatedAt = null; + + #[FieldAttribute(fieldName: 'mydeleted_at', syncWithDb: false)] + protected ?string $deletedAt = null; #[FieldAttribute(fieldName: 'myrole')] protected ?string $role = null; diff --git a/tests/Fixture/MyUserModel.php b/tests/Fixture/MyUserModel.php index bc79e38..192ed98 100644 --- a/tests/Fixture/MyUserModel.php +++ b/tests/Fixture/MyUserModel.php @@ -7,6 +7,7 @@ use ByJG\MicroOrm\Attributes\FieldAttribute; use ByJG\MicroOrm\Attributes\TableAttribute; use ByJG\MicroOrm\Literal\HexUuidLiteral; +use ByJG\MicroOrm\MapperFunctions\NowUtcMapper; use ByJG\MicroOrm\MapperFunctions\ReadOnlyMapper; #[TableAttribute(tableName: 'mytable')] @@ -27,8 +28,14 @@ class MyUserModel extends UserModel #[FieldAttribute(fieldName: 'mypassword', updateFunction: PasswordSha1Mapper::class)] protected ?string $password = null; - #[FieldAttribute(fieldName: 'mycreated', updateFunction: ReadOnlyMapper::class)] - protected ?string $created = null; + #[FieldAttribute(fieldName: 'mycreated_at', updateFunction: ReadOnlyMapper::class, insertFunction: NowUtcMapper::class)] + protected ?string $createdAt = null; + + #[FieldAttribute(fieldName: 'myupdated_at', updateFunction: NowUtcMapper::class)] + protected ?string $updatedAt = null; + + #[FieldAttribute(fieldName: 'mydeleted_at', syncWithDb: false)] + protected ?string $deletedAt = null; #[FieldAttribute(fieldName: 'myrole')] protected ?string $role = null; diff --git a/tests/Fixture/UserModelMd5.php b/tests/Fixture/UserModelMd5.php index dfdd702..ce19bf0 100644 --- a/tests/Fixture/UserModelMd5.php +++ b/tests/Fixture/UserModelMd5.php @@ -5,6 +5,7 @@ use ByJG\Authenticate\Model\UserModel; use ByJG\MicroOrm\Attributes\FieldAttribute; use ByJG\MicroOrm\Attributes\TableAttribute; +use ByJG\MicroOrm\MapperFunctions\NowUtcMapper; use ByJG\MicroOrm\MapperFunctions\ReadOnlyMapper; #[TableAttribute(tableName: 'users')] @@ -25,8 +26,14 @@ class UserModelMd5 extends UserModel #[FieldAttribute(updateFunction: PasswordMd5Mapper::class)] protected ?string $password = null; - #[FieldAttribute(updateFunction: ReadOnlyMapper::class)] - protected ?string $created = null; + #[FieldAttribute(fieldName: 'created_at', updateFunction: ReadOnlyMapper::class, insertFunction: NowUtcMapper::class)] + protected ?string $createdAt = null; + + #[FieldAttribute(fieldName: 'updated_at', updateFunction: NowUtcMapper::class)] + protected ?string $updatedAt = null; + + #[FieldAttribute(fieldName: 'deleted_at', syncWithDb: false)] + protected ?string $deletedAt = null; #[FieldAttribute] protected ?string $role = null; diff --git a/tests/PasswordMd5MapperTest.php b/tests/PasswordMd5MapperTest.php index 1c84eec..1210538 100644 --- a/tests/PasswordMd5MapperTest.php +++ b/tests/PasswordMd5MapperTest.php @@ -30,7 +30,9 @@ public function setUp(): void email varchar(200), username varchar(20), password varchar(40), - created datetime default (datetime(\'2017-12-04\')), + created_at datetime default (datetime(\'2017-12-04\')), + updated_at datetime, + deleted_at datetime, role varchar(20));' ); diff --git a/tests/TestUsersBase.php b/tests/TestUsersBase.php index 9dda3f6..b57916d 100644 --- a/tests/TestUsersBase.php +++ b/tests/TestUsersBase.php @@ -217,13 +217,18 @@ protected function expectedToken($tokenData, $login, $userId): void $dataFromToken->login = $loginCreated; $dataFromToken->userid = $userId; - $this->assertEquals( - [ - 'user' => $user, - 'data' => $dataFromToken - ], - $this->object->isValidToken($loginCreated, $jwtWrapper, $token) - ); + $tokenResult = $this->object->isValidToken($loginCreated, $jwtWrapper, $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->getName(), $tokenResult['user']->getName()); + $this->assertEquals($user->getEmail(), $tokenResult['user']->getEmail()); + $this->assertEquals($user->getUsername(), $tokenResult['user']->getUsername()); + $this->assertEquals($user->getPassword(), $tokenResult['user']->getPassword()); + $this->assertEquals($user->getRole(), $tokenResult['user']->getRole()); } /** @@ -334,8 +339,8 @@ public function testPasswordDefinitionValidOnSave(): void $user->setName('Test User'); $user->setUsername('testuser_pwd'); $user->setEmail('testpwd@example.com'); - $user->withPasswordDefinition($passwordDef); $user->setPassword('ValidPass8642'); // Valid: uppercase, lowercase, numbers, no sequential, 12 chars + $user->withPasswordDefinition($passwordDef); // Should save successfully $savedUser = $this->object->save($user); @@ -387,8 +392,8 @@ public function testPasswordDefinitionValidOnUpdate(): void ]); // Update password with valid value - $user->withPasswordDefinition($passwordDef); $user->setPassword('StrongPass84!'); // Valid: uppercase, lowercase, 2 numbers, symbol, no sequential, 13 chars + $user->withPasswordDefinition($passwordDef); // Should update successfully $savedUser = $this->object->save($user); @@ -399,6 +404,7 @@ public function testPasswordDefinitionValidOnUpdate(): void $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 @@ -501,4 +507,50 @@ public function testPasswordDefinitionViaServiceAddUser(): void '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/UsersDBDataset2ByUserNameTestUsersBase.php b/tests/UsersDBDataset2ByUserNameTestUsersBase.php index 2b8e8a0..b9e59bd 100644 --- a/tests/UsersDBDataset2ByUserNameTestUsersBase.php +++ b/tests/UsersDBDataset2ByUserNameTestUsersBase.php @@ -28,7 +28,9 @@ public function __setUp($loginField) myemail varchar(200), myusername varchar(20), mypassword varchar(40), - mycreated datetime default (datetime(\'2017-12-04\')), + mycreated_at datetime default (datetime(\'2017-12-04\')), + myupdated_at datetime, + mydeleted_at datetime, myrole varchar(20));' ); @@ -84,7 +86,8 @@ public function testAddUser() $this->assertEquals('johndoe@gmail.com', $user->getEmail()); $this->assertEquals('91dfd9ddb4198affc5c194cd8ce6d338fde470e2', $user->getPassword()); $this->assertEquals('', $user->getRole()); - $this->assertEquals('2017-12-04 00:00:00', $user->getCreated()); + $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'); @@ -115,6 +118,14 @@ public function testSaveAndSave() $user2 = $this->object->getById("1"); - $this->assertEquals($user, $user2); + // 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/UsersDBDatasetByUsernameTestUsersBase.php b/tests/UsersDBDatasetByUsernameTestUsersBase.php index 68686b7..c5d098f 100644 --- a/tests/UsersDBDatasetByUsernameTestUsersBase.php +++ b/tests/UsersDBDatasetByUsernameTestUsersBase.php @@ -31,7 +31,9 @@ public function __setUp($loginField) email varchar(200), username varchar(20), password varchar(40), - created datetime default (datetime(\'2017-12-04\')), + created_at datetime default (datetime(\'2017-12-04\')), + updated_at datetime, + deleted_at datetime, role varchar(20));' ); @@ -57,7 +59,8 @@ public function __setUp($loginField) $this->assertEquals('user1', $user->getUsername()); $this->assertEquals('a63d4b132a9a1d3430f9ae507825f572449e0d17', $user->getPassword()); $this->assertEquals('', $user->getRole()); - $this->assertEquals('2017-12-04 00:00:00', $user->getCreated()); + $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'); @@ -94,7 +97,8 @@ public function testAddUser() $this->assertEquals('johndoe@gmail.com', $user->getEmail()); $this->assertEquals('91dfd9ddb4198affc5c194cd8ce6d338fde470e2', $user->getPassword()); $this->assertEquals('', $user->getRole()); - $this->assertEquals('2017-12-04 00:00:00', $user->getCreated()); + $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'); @@ -141,6 +145,14 @@ public function testSaveAndSave() $user2 = $this->object->getById("1"); - $this->assertEquals($user, $user2); + // 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 3ba1928..6818357 100644 --- a/tests/UsersDBDatasetDefinitionTest.php +++ b/tests/UsersDBDatasetDefinitionTest.php @@ -44,7 +44,9 @@ public function __setUp($loginField) myusername varchar(20), mypassword varchar(40), myotherfield varchar(40), - mycreated datetime default (datetime(\'2017-12-04\')), + mycreated_at datetime default (datetime(\'2017-12-04\')), + myupdated_at datetime, + mydeleted_at datetime, myrole varchar(20));' ); @@ -119,7 +121,8 @@ public function testAddUser() $this->assertEquals('', $user->getRole()); /** @psalm-suppress UndefinedMethod Check UserModel::__call */ $this->assertEquals('other john', $user->getOtherfield()); - $this->assertEquals('2017-12-04 00:00:00', $user->getCreated()); // Database default value + $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'); From 917ddf79e09039f6729b235e3d0f34030c954405 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Sat, 15 Nov 2025 23:14:33 -0500 Subject: [PATCH 23/40] Remove unused entity processors (`ClosureEntityProcessor`, `PassThroughEntityProcessor`), `NotImplementedException` class, update exceptions in `UsersRepository`, and refactor `UsersServiceInterface` namespace for better alignment. --- .../ClosureEntityProcessor.php | 32 ------------------- .../PassThroughEntityProcessor.php | 17 ---------- src/Exception/NotImplementedException.php | 10 ------ .../UsersServiceInterface.php | 2 +- src/Repository/UsersRepository.php | 21 ++++++++++++ src/Service/UsersService.php | 1 + 6 files changed, 23 insertions(+), 60 deletions(-) delete mode 100644 src/EntityProcessors/ClosureEntityProcessor.php delete mode 100644 src/EntityProcessors/PassThroughEntityProcessor.php delete mode 100644 src/Exception/NotImplementedException.php rename src/{Service => Interfaces}/UsersServiceInterface.php (99%) diff --git a/src/EntityProcessors/ClosureEntityProcessor.php b/src/EntityProcessors/ClosureEntityProcessor.php deleted file mode 100644 index b1bc747..0000000 --- a/src/EntityProcessors/ClosureEntityProcessor.php +++ /dev/null @@ -1,32 +0,0 @@ -closure = $closure; - } - - #[\Override] - public function process(array $instance): array - { - $result = ($this->closure)($instance); - - // If closure returns an object, convert it to array - if (is_object($result)) { - return (array) $result; - } - - return $result; - } -} diff --git a/src/EntityProcessors/PassThroughEntityProcessor.php b/src/EntityProcessors/PassThroughEntityProcessor.php deleted file mode 100644 index d1aef84..0000000 --- a/src/EntityProcessors/PassThroughEntityProcessor.php +++ /dev/null @@ -1,17 +0,0 @@ - Date: Sat, 15 Nov 2025 23:48:07 -0500 Subject: [PATCH 24/40] Refactor login field handling: replace string constants with `LoginField` enum, update `UsersService`, tests, and repository field mappings for consistency and validation. --- src/Enum/LoginField.php | 9 +++ src/Enum/User.php | 17 ++++ src/Enum/UserProperty.php | 12 +++ src/Service/UsersService.php | 80 +++++++++++++------ tests/PasswordMd5MapperTest.php | 3 +- tests/TestUsersBase.php | 16 ++-- tests/UsersDBDataset2ByEmailTest.php | 4 +- ...UsersDBDataset2ByUserNameTestUsersBase.php | 3 +- tests/UsersDBDatasetByEmailTest.php | 4 +- .../UsersDBDatasetByUsernameTestUsersBase.php | 3 +- tests/UsersDBDatasetDefinitionTest.php | 3 +- 11 files changed, 112 insertions(+), 42 deletions(-) create mode 100644 src/Enum/LoginField.php create mode 100644 src/Enum/User.php create mode 100644 src/Enum/UserProperty.php 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 @@ +usersRepository = $usersRepository; $this->propertiesRepository = $propertiesRepository; - $this->loginField = $loginField; $this->passwordDefinition = $passwordDefinition; + $this->loginField = $loginField; + + $userMapper = $usersRepository->getRepository()->getMapper(); + $userCheck = ($userMapper->getFieldMap(User::Userid->value) !== null) && + ($userMapper->getFieldMap(User::Name->value) !== null) && + ($userMapper->getFieldMap(User::Email->value) !== null) && + ($userMapper->getFieldMap(User::Username->value) !== null) && + ($userMapper->getFieldMap(User::Password->value) !== null) && + ($userMapper->getFieldMap(User::Role->value) !== null); + + if (!$userCheck) { + throw new InvalidArgumentException('Invalid user repository field mappings'); + } + + $propertyMapper = $propertiesRepository->getRepository()->getMapper(); + $propertyCheck = ($propertyMapper->getFieldMap(UserProperty::Userid->value) !== null) && + ($propertyMapper->getFieldMap(UserProperty::Name->value) !== null) && + ($propertyMapper->getFieldMap(UserProperty::Value->value) !== null); + + if (!$propertyCheck) { + throw new InvalidArgumentException('Invalid property repository field mappings'); + } } /** - * Get the users repository + * Get the user's repository * * @return UsersRepository */ @@ -53,7 +76,7 @@ public function getUsersRepository(): UsersRepository } /** - * Get the properties repository + * Get the property repository * * @return UserPropertiesRepository */ @@ -98,10 +121,11 @@ public function save(UserModel $model): UserModel public function addUser(string $name, string $userName, string $email, string $password): UserModel { - $model = $this->usersRepository->getMapper()->getEntity([ - 'name' => $name, - 'email' => $email, - 'username' => $userName, + $mapper = $this->usersRepository->getMapper(); + $model = $mapper->getEntity([ + $mapper->getFieldMap(User::Name->value)->getFieldName() => $name, + $mapper->getFieldMap(User::Email->value)->getFieldName() => $email, + $mapper->getFieldMap(User::Username->value)->getFieldName() => $userName, ]); if ($this->passwordDefinition !== null) { $model->withPasswordDefinition($this->passwordDefinition); @@ -148,7 +172,8 @@ public function getById(string|HexUuidLiteral|int $userid): ?UserModel */ public function getByEmail(string $email): ?UserModel { - $user = $this->usersRepository->getByField('email', $email); + $fieldMap = $this->usersRepository->getMapper()->getFieldMap(User::Email->value); + $user = $this->usersRepository->getByField($fieldMap->getFieldName(), $email); if ($user !== null) { $this->loadUserProperties($user); } @@ -160,7 +185,8 @@ public function getByEmail(string $email): ?UserModel */ public function getByUsername(string $username): ?UserModel { - $user = $this->usersRepository->getByField('username', $username); + $fieldMap = $this->usersRepository->getMapper()->getFieldMap(User::Username->value); + $user = $this->usersRepository->getByField($fieldMap->getFieldName(), $username); if ($user !== null) { $this->loadUserProperties($user); } @@ -172,7 +198,7 @@ public function getByUsername(string $username): ?UserModel */ public function getByLogin(string $login): ?UserModel { - return $this->loginField === self::LOGIN_IS_EMAIL + return $this->loginField === LoginField::Email ? $this->getByEmail($login) : $this->getByUsername($login); } @@ -229,7 +255,7 @@ public function isValidUser(string $login, string $password): ?UserModel } // Hash the password for comparison using the model's configured password mapper - $passwordFieldMapping = $this->usersRepository->getMapper()->getFieldMap('password'); + $passwordFieldMapping = $this->usersRepository->getMapper()->getFieldMap(User::Password->value); $hashedPassword = $passwordFieldMapping->getUpdateFunctionValue($password, null); if ($user->getPassword() === $hashedPassword) { @@ -297,10 +323,11 @@ public function addProperty(string|HexUuidLiteral|int $userId, string $propertyN } if (!$this->hasProperty($userId, $propertyName, $value)) { - $property = $this->propertiesRepository->getMapper()->getEntity([ - 'userid' => $userId, - 'name' => $propertyName, - 'value' => $value + $propMapper = $this->propertiesRepository->getMapper(); + $property = $propMapper->getEntity([ + $propMapper->getFieldMap(UserProperty::Userid->value)->getFieldName() => $userId, + $propMapper->getFieldMap(UserProperty::Name->value)->getFieldName() => $propertyName, + $propMapper->getFieldMap(UserProperty::Value->value)->getFieldName() => $value ]); $this->propertiesRepository->save($property); } @@ -316,10 +343,11 @@ public function setProperty(string|HexUuidLiteral|int $userId, string $propertyN $properties = $this->propertiesRepository->getByUserIdAndName($userId, $propertyName); if (empty($properties)) { - $property = $this->propertiesRepository->getMapper()->getEntity([ - 'userid' => $userId, - 'name' => $propertyName, - 'value' => $value + $propMapper = $this->propertiesRepository->getMapper(); + $property = $propMapper->getEntity([ + $propMapper->getFieldMap(UserProperty::Userid->value)->getFieldName() => $userId, + $propMapper->getFieldMap(UserProperty::Name->value)->getFieldName() => $propertyName, + $propMapper->getFieldMap(UserProperty::Value->value)->getFieldName() => $value ]); } else { $property = $properties[0]; @@ -373,9 +401,9 @@ public function getUsersByPropertySet(array $propertiesArray): array $userPk = $userPk[0]; } - $propUserIdField = $this->propertiesRepository->getMapper()->getFieldMap('userid')->getFieldName(); - $propNameField = $this->propertiesRepository->getMapper()->getFieldMap('name')->getFieldName(); - $propValueField = $this->propertiesRepository->getMapper()->getFieldMap('value')->getFieldName(); + $propUserIdField = $this->propertiesRepository->getMapper()->getFieldMap(UserProperty::Userid->value)->getFieldName(); + $propNameField = $this->propertiesRepository->getMapper()->getFieldMap(UserProperty::Name->value)->getFieldName(); + $propValueField = $this->propertiesRepository->getMapper()->getFieldMap(UserProperty::Value->value)->getFieldName(); $query = Query::getInstance() ->field("u.*") diff --git a/tests/PasswordMd5MapperTest.php b/tests/PasswordMd5MapperTest.php index 1210538..6257082 100644 --- a/tests/PasswordMd5MapperTest.php +++ b/tests/PasswordMd5MapperTest.php @@ -4,6 +4,7 @@ use ByJG\AnyDataset\Db\DatabaseExecutor; use ByJG\AnyDataset\Db\Factory; +use ByJG\Authenticate\Enum\LoginField; use ByJG\Authenticate\Model\UserPropertiesModel; use ByJG\Authenticate\Repository\UserPropertiesRepository; use ByJG\Authenticate\Repository\UsersRepository; @@ -50,7 +51,7 @@ public function setUp(): void $this->service = new UsersService( $usersRepository, $propertiesRepository, - UsersService::LOGIN_IS_USERNAME + LoginField::Username ); } diff --git a/tests/TestUsersBase.php b/tests/TestUsersBase.php index b57916d..da922a1 100644 --- a/tests/TestUsersBase.php +++ b/tests/TestUsersBase.php @@ -2,6 +2,7 @@ namespace Tests; +use ByJG\Authenticate\Enum\LoginField; use ByJG\Authenticate\Exception\NotAuthenticatedException; use ByJG\Authenticate\Exception\UserExistsException; use ByJG\Authenticate\Service\UsersService; @@ -16,7 +17,7 @@ abstract class TestUsersBase extends TestCase */ protected UsersService|null $object = null; - protected string $loginField; + protected LoginField $loginField; protected $prefix = ""; @@ -24,17 +25,16 @@ abstract public function __setUp($loginField); public function __chooseValue($forUsername, $forEmail): string { - $searchForList = [ - 'email' => $forEmail, - 'username' => $forUsername, - ]; - return $searchForList[$this->loginField]; + return match ($this->loginField) { + LoginField::Email => $forEmail, + LoginField::Username => $forUsername, + }; } #[\Override] public function setUp(): void { - $this->__setUp(UsersService::LOGIN_IS_USERNAME); + $this->__setUp(LoginField::Username); } /** @@ -483,7 +483,7 @@ public function testPasswordDefinitionViaServiceAddUser(): void $usersWithPwdDef = new \ByJG\Authenticate\Service\UsersService( $usersRepo, $propsRepo, - \ByJG\Authenticate\Service\UsersService::LOGIN_IS_USERNAME, + LoginField::Username, $passwordDef ); diff --git a/tests/UsersDBDataset2ByEmailTest.php b/tests/UsersDBDataset2ByEmailTest.php index 5fb9809..4c3e692 100644 --- a/tests/UsersDBDataset2ByEmailTest.php +++ b/tests/UsersDBDataset2ByEmailTest.php @@ -2,13 +2,13 @@ namespace Tests; -use ByJG\Authenticate\Service\UsersService; +use ByJG\Authenticate\Enum\LoginField; class UsersDBDataset2ByEmailTest extends UsersDBDataset2ByUserNameTestUsersBase { #[\Override] public function setUp(): void { - $this->__setUp(UsersService::LOGIN_IS_EMAIL); + $this->__setUp(LoginField::Email); } } diff --git a/tests/UsersDBDataset2ByUserNameTestUsersBase.php b/tests/UsersDBDataset2ByUserNameTestUsersBase.php index b9e59bd..8d2469f 100644 --- a/tests/UsersDBDataset2ByUserNameTestUsersBase.php +++ b/tests/UsersDBDataset2ByUserNameTestUsersBase.php @@ -4,6 +4,7 @@ use ByJG\AnyDataset\Db\DatabaseExecutor; use ByJG\AnyDataset\Db\Factory; +use ByJG\Authenticate\Enum\LoginField; use ByJG\Authenticate\Repository\UserPropertiesRepository; use ByJG\Authenticate\Repository\UsersRepository; use ByJG\Authenticate\Service\UsersService; @@ -58,7 +59,7 @@ public function __setUp($loginField) #[\Override] public function setUp(): void { - $this->__setUp(UsersService::LOGIN_IS_USERNAME); + $this->__setUp(LoginField::Username); } #[\Override] diff --git a/tests/UsersDBDatasetByEmailTest.php b/tests/UsersDBDatasetByEmailTest.php index e49d400..2f166e0 100644 --- a/tests/UsersDBDatasetByEmailTest.php +++ b/tests/UsersDBDatasetByEmailTest.php @@ -2,13 +2,13 @@ namespace Tests; -use ByJG\Authenticate\Service\UsersService; +use ByJG\Authenticate\Enum\LoginField; class UsersDBDatasetByEmailTest extends UsersDBDatasetByUsernameTestUsersBase { #[\Override] public function setUp(): void { - $this->__setUp(UsersService::LOGIN_IS_EMAIL); + $this->__setUp(LoginField::Email); } } diff --git a/tests/UsersDBDatasetByUsernameTestUsersBase.php b/tests/UsersDBDatasetByUsernameTestUsersBase.php index c5d098f..2a71edc 100644 --- a/tests/UsersDBDatasetByUsernameTestUsersBase.php +++ b/tests/UsersDBDatasetByUsernameTestUsersBase.php @@ -4,6 +4,7 @@ use ByJG\AnyDataset\Db\DatabaseExecutor; use ByJG\AnyDataset\Db\Factory; +use ByJG\Authenticate\Enum\LoginField; use ByJG\Authenticate\Model\UserModel; use ByJG\Authenticate\Model\UserPropertiesModel; use ByJG\Authenticate\Repository\UserPropertiesRepository; @@ -69,7 +70,7 @@ public function __setUp($loginField) #[\Override] public function setUp(): void { - $this->__setUp(UsersService::LOGIN_IS_USERNAME); + $this->__setUp(LoginField::Username); } #[\Override] diff --git a/tests/UsersDBDatasetDefinitionTest.php b/tests/UsersDBDatasetDefinitionTest.php index 6818357..e1d9d8b 100644 --- a/tests/UsersDBDatasetDefinitionTest.php +++ b/tests/UsersDBDatasetDefinitionTest.php @@ -5,6 +5,7 @@ use ByJG\AnyDataset\Core\Exception\DatabaseException; use ByJG\AnyDataset\Db\DatabaseExecutor; use ByJG\AnyDataset\Db\Factory; +use ByJG\Authenticate\Enum\LoginField; use ByJG\Authenticate\Exception\UserExistsException; use ByJG\Authenticate\Repository\UserPropertiesRepository; use ByJG\Authenticate\Repository\UsersRepository; @@ -87,7 +88,7 @@ public function __setUp($loginField) #[Override] public function setUp(): void { - $this->__setUp(UsersService::LOGIN_IS_USERNAME); + $this->__setUp(LoginField::Username); } #[\Override] From 02032b9c10bb33a44121eb8798bf1c92d4e84729 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Sat, 15 Nov 2025 23:57:22 -0500 Subject: [PATCH 25/40] Add #[\Override] attribute to applicable methods and update type declarations for consistency in `UsersService`, `PasswordMd5Mapper`, and related tests. --- src/Definition/PasswordDefinition.php | 2 +- src/Service/UsersService.php | 20 ++++++++++++++++++++ tests/Fixture/PasswordMd5Mapper.php | 1 + tests/TestUsersBase.php | 3 +-- 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/Definition/PasswordDefinition.php b/src/Definition/PasswordDefinition.php index ad2c644..61ad5b4 100644 --- a/src/Definition/PasswordDefinition.php +++ b/src/Definition/PasswordDefinition.php @@ -48,7 +48,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"); diff --git a/src/Service/UsersService.php b/src/Service/UsersService.php index b68991d..0d36e6e 100644 --- a/src/Service/UsersService.php +++ b/src/Service/UsersService.php @@ -88,6 +88,7 @@ public function getPropertiesRepository(): UserPropertiesRepository /** * @inheritDoc */ + #[\Override] public function save(UserModel $model): UserModel { $newUser = false; @@ -118,6 +119,7 @@ public function save(UserModel $model): UserModel /** * @inheritDoc */ + #[\Override] public function addUser(string $name, string $userName, string $email, string $password): UserModel { @@ -158,6 +160,7 @@ protected function canAddUser(UserModel $model): bool /** * @inheritDoc */ + #[\Override] public function getById(string|HexUuidLiteral|int $userid): ?UserModel { $user = $this->usersRepository->getById($userid); @@ -170,6 +173,7 @@ public function getById(string|HexUuidLiteral|int $userid): ?UserModel /** * @inheritDoc */ + #[\Override] public function getByEmail(string $email): ?UserModel { $fieldMap = $this->usersRepository->getMapper()->getFieldMap(User::Email->value); @@ -183,6 +187,7 @@ public function getByEmail(string $email): ?UserModel /** * @inheritDoc */ + #[\Override] public function getByUsername(string $username): ?UserModel { $fieldMap = $this->usersRepository->getMapper()->getFieldMap(User::Username->value); @@ -196,6 +201,7 @@ public function getByUsername(string $username): ?UserModel /** * @inheritDoc */ + #[\Override] public function getByLogin(string $login): ?UserModel { return $this->loginField === LoginField::Email @@ -218,6 +224,7 @@ protected function loadUserProperties(UserModel $user): void /** * @inheritDoc */ + #[\Override] public function removeByLogin(string $login): bool { $user = $this->getByLogin($login); @@ -230,6 +237,7 @@ public function removeByLogin(string $login): bool /** * @inheritDoc */ + #[\Override] public function removeById(string|HexUuidLiteral|int $userid): bool { try { @@ -246,6 +254,7 @@ public function removeById(string|HexUuidLiteral|int $userid): bool /** * @inheritDoc */ + #[\Override] public function isValidUser(string $login, string $password): ?UserModel { $user = $this->getByLogin($login); @@ -268,6 +277,7 @@ public function isValidUser(string $login, string $password): ?UserModel /** * @inheritDoc */ + #[\Override] public function hasProperty(string|int|HexUuidLiteral $userId, string $propertyName, ?string $value = null): bool { $user = $this->getById($userId); @@ -292,6 +302,7 @@ public function hasProperty(string|int|HexUuidLiteral $userId, string $propertyN /** * @inheritDoc */ + #[\Override] public function getProperty(string|HexUuidLiteral|int $userId, string $propertyName): array|string|UserPropertiesModel|null { $properties = $this->propertiesRepository->getByUserIdAndName($userId, $propertyName); @@ -315,6 +326,7 @@ public function getProperty(string|HexUuidLiteral|int $userId, string $propertyN /** * @inheritDoc */ + #[\Override] public function addProperty(string|HexUuidLiteral|int $userId, string $propertyName, ?string $value): bool { $user = $this->getById($userId); @@ -338,6 +350,7 @@ public function addProperty(string|HexUuidLiteral|int $userId, string $propertyN /** * @inheritDoc */ + #[\Override] public function setProperty(string|HexUuidLiteral|int $userId, string $propertyName, ?string $value): bool { $properties = $this->propertiesRepository->getByUserIdAndName($userId, $propertyName); @@ -361,6 +374,7 @@ public function setProperty(string|HexUuidLiteral|int $userId, string $propertyN /** * @inheritDoc */ + #[\Override] public function removeProperty(string|HexUuidLiteral|int $userId, string $propertyName, ?string $value = null): bool { $user = $this->getById($userId); @@ -374,6 +388,7 @@ public function removeProperty(string|HexUuidLiteral|int $userId, string $proper /** * @inheritDoc */ + #[\Override] public function removeAllProperties(string $propertyName, ?string $value = null): void { $this->propertiesRepository->deleteByName($propertyName, $value); @@ -382,6 +397,7 @@ public function removeAllProperties(string $propertyName, ?string $value = null) /** * @inheritDoc */ + #[\Override] public function getUsersByProperty(string $propertyName, string $value): array { return $this->getUsersByPropertySet([$propertyName => $value]); @@ -390,6 +406,7 @@ public function getUsersByProperty(string $propertyName, string $value): array /** * @inheritDoc */ + #[\Override] public function getUsersByPropertySet(array $propertiesArray): array { $userTable = $this->usersRepository->getTableName(); @@ -423,6 +440,7 @@ public function getUsersByPropertySet(array $propertiesArray): array /** * @inheritDoc */ + #[\Override] public function createAuthToken( string $login, string $password, @@ -458,6 +476,7 @@ public function createAuthToken( * @throws NotAuthenticatedException * @throws UserNotFoundException */ + #[\Override] public function isValidToken(string $login, JwtWrapper $jwtWrapper, string $token): ?array { $user = $this->getByLogin($login); @@ -480,6 +499,7 @@ public function isValidToken(string $login, JwtWrapper $jwtWrapper, string $toke ]; } + #[\Override] public function getUsersEntity(array $fields): UserModel { return $this->getUsersRepository()->getMapper()->getEntity($fields); diff --git a/tests/Fixture/PasswordMd5Mapper.php b/tests/Fixture/PasswordMd5Mapper.php index 96cbb1f..dde5a50 100644 --- a/tests/Fixture/PasswordMd5Mapper.php +++ b/tests/Fixture/PasswordMd5Mapper.php @@ -9,6 +9,7 @@ */ class PasswordMd5Mapper implements MapperFunctionInterface { + #[\Override] public function processedValue(mixed $value, mixed $instance, mixed $executor = null): mixed { diff --git a/tests/TestUsersBase.php b/tests/TestUsersBase.php index da922a1..08d089b 100644 --- a/tests/TestUsersBase.php +++ b/tests/TestUsersBase.php @@ -195,7 +195,7 @@ public function testIsAdmin(): void $this->assertEquals('moderator', $user3->getRole()); } - protected function expectedToken($tokenData, $login, $userId): void + protected function expectedToken(string $tokenData, string $login, int $userId): void { $loginCreated = $this->__chooseValue('user2', 'user2@gmail.com'); @@ -397,7 +397,6 @@ public function testPasswordDefinitionValidOnUpdate(): void // Should update successfully $savedUser = $this->object->save($user); - $this->assertNotNull($savedUser); // Verify password was updated (by checking authentication works with login field) $login = $this->__chooseValue($user->getUsername(), $user->getEmail()); From bf809f0247ebb78930735d9aa63429401b82288c Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Sun, 16 Nov 2025 00:49:10 -0500 Subject: [PATCH 26/40] Replace `HexUuidLiteral` with `Literal` in model attributes, method signatures, and dependencies across user-related classes, repositories, services, and tests for consistency. Update `PasswordSha1Mapper` with `isPasswordEncrypted` method to streamline validation logic. --- docs/database-storage.md | 8 +++--- src/Interfaces/UsersServiceInterface.php | 30 ++++++++++----------- src/MapperFunctions/PasswordSha1Mapper.php | 11 +++++--- src/Model/UserModel.php | 12 ++++----- src/Model/UserPropertiesModel.php | 12 ++++----- src/Repository/UserPropertiesRepository.php | 18 ++++++------- src/Repository/UsersRepository.php | 14 +++++----- src/Service/UsersService.php | 16 +++++------ tests/CustomUserModel.php | 2 +- tests/CustomUserPropertiesModel.php | 2 +- tests/Fixture/MyUserModel.php | 4 +-- tests/Fixture/MyUserPropertiesModel.php | 2 +- tests/Fixture/UserModelMd5.php | 2 +- 13 files changed, 69 insertions(+), 64 deletions(-) diff --git a/docs/database-storage.md b/docs/database-storage.md index 70cdc74..b6dc5c2 100644 --- a/docs/database-storage.md +++ b/docs/database-storage.md @@ -90,13 +90,13 @@ use ByJG\Authenticate\MapperFunctions\PasswordSha1Mapper; use ByJG\MicroOrm\Attributes\FieldAttribute; use ByJG\MicroOrm\Attributes\TableAttribute; use ByJG\MicroOrm\MapperFunctions\ReadOnlyMapper; -use ByJG\MicroOrm\Literal\HexUuidLiteral; +use ByJG\MicroOrm\Literal\Literal; #[TableAttribute(tableName: 'my_users_table')] class CustomUserModel extends UserModel { #[FieldAttribute(fieldName: 'user_id', primaryKey: true)] - protected string|int|HexUuidLiteral|null $userid = null; + protected string|int|Literal|null $userid = null; #[FieldAttribute(fieldName: 'full_name')] protected ?string $name = null; @@ -134,7 +134,7 @@ $usersRepo = new UsersRepository($db, CustomUserModel::class); use ByJG\Authenticate\Model\UserPropertiesModel; use ByJG\MicroOrm\Attributes\FieldAttribute; use ByJG\MicroOrm\Attributes\TableAttribute; -use ByJG\MicroOrm\Literal\HexUuidLiteral; +use ByJG\MicroOrm\Literal\Literal; #[TableAttribute(tableName: 'custom_properties')] class CustomPropertiesModel extends UserPropertiesModel @@ -149,7 +149,7 @@ class CustomPropertiesModel extends UserPropertiesModel protected ?string $value = null; #[FieldAttribute(fieldName: 'user_id')] - protected string|int|HexUuidLiteral|null $userid = null; + protected string|int|Literal|null $userid = null; } // Use custom model diff --git a/src/Interfaces/UsersServiceInterface.php b/src/Interfaces/UsersServiceInterface.php index 2b89dec..e86fcd8 100644 --- a/src/Interfaces/UsersServiceInterface.php +++ b/src/Interfaces/UsersServiceInterface.php @@ -5,7 +5,7 @@ use ByJG\Authenticate\Model\UserModel; use ByJG\Authenticate\Model\UserPropertiesModel; use ByJG\JwtWrapper\JwtWrapper; -use ByJG\MicroOrm\Literal\HexUuidLiteral; +use ByJG\MicroOrm\Literal\Literal; /** * Interface for Users Service @@ -34,10 +34,10 @@ public function addUser(string $name, string $userName, string $email, string $p /** * Get user by ID * - * @param string|HexUuidLiteral|int $userid + * @param string|Literal|int $userid * @return UserModel|null */ - public function getById(string|HexUuidLiteral|int $userid): ?UserModel; + public function getById(string|Literal|int $userid): ?UserModel; /** * Get user by email @@ -74,10 +74,10 @@ public function removeByLogin(string $login): bool; /** * Remove user by ID * - * @param string|HexUuidLiteral|int $userid + * @param string|Literal|int $userid * @return bool */ - public function removeById(string|HexUuidLiteral|int $userid): bool; + public function removeById(string|Literal|int $userid): bool; /** * Validate if user and password are correct @@ -91,51 +91,51 @@ public function isValidUser(string $login, string $password): ?UserModel; /** * Check if user has a property * - * @param string|int|HexUuidLiteral $userId + * @param string|int|Literal $userId * @param string $propertyName * @param string|null $value * @return bool */ - public function hasProperty(string|int|HexUuidLiteral $userId, string $propertyName, ?string $value = null): bool; + public function hasProperty(string|int|Literal $userId, string $propertyName, ?string $value = null): bool; /** * Get property value(s) for a user * - * @param string|HexUuidLiteral|int $userId + * @param string|Literal|int $userId * @param string $propertyName * @return array|string|UserPropertiesModel|null */ - public function getProperty(string|HexUuidLiteral|int $userId, string $propertyName): array|string|UserPropertiesModel|null; + public function getProperty(string|Literal|int $userId, string $propertyName): array|string|UserPropertiesModel|null; /** * Add a property to a user * - * @param string|HexUuidLiteral|int $userId + * @param string|Literal|int $userId * @param string $propertyName * @param string|null $value * @return bool */ - public function addProperty(string|HexUuidLiteral|int $userId, string $propertyName, ?string $value): bool; + public function addProperty(string|Literal|int $userId, string $propertyName, ?string $value): bool; /** * Set a property (replaces existing) * - * @param string|HexUuidLiteral|int $userId + * @param string|Literal|int $userId * @param string $propertyName * @param string|null $value * @return bool */ - public function setProperty(string|HexUuidLiteral|int $userId, string $propertyName, ?string $value): bool; + public function setProperty(string|Literal|int $userId, string $propertyName, ?string $value): bool; /** * Remove a property from a user * - * @param string|HexUuidLiteral|int $userId + * @param string|Literal|int $userId * @param string $propertyName * @param string|null $value * @return bool */ - public function removeProperty(string|HexUuidLiteral|int $userId, string $propertyName, ?string $value = null): bool; + public function removeProperty(string|Literal|int $userId, string $propertyName, ?string $value = null): bool; /** * Remove a property from all users diff --git a/src/MapperFunctions/PasswordSha1Mapper.php b/src/MapperFunctions/PasswordSha1Mapper.php index 97c2bac..bdd1ffb 100644 --- a/src/MapperFunctions/PasswordSha1Mapper.php +++ b/src/MapperFunctions/PasswordSha1Mapper.php @@ -3,18 +3,18 @@ namespace ByJG\Authenticate\MapperFunctions; use ByJG\AnyDataset\Db\DatabaseExecutor; -use ByJG\MicroOrm\Interface\MapperFunctionInterface; +use ByJG\Authenticate\Interfaces\PasswordUpdaterInterface; /** * Mapper function to hash passwords using SHA1 */ -class PasswordSha1Mapper implements MapperFunctionInterface +class PasswordSha1Mapper implements PasswordUpdaterInterface { #[\Override] public function processedValue(mixed $value, mixed $instance, ?DatabaseExecutor $executor = null): mixed { // Already have a SHA1 password (40 characters) - if (is_string($value) && strlen($value) === 40) { + if ($this->isPasswordEncrypted($value)) { return $value; } @@ -26,4 +26,9 @@ public function processedValue(mixed $value, mixed $instance, ?DatabaseExecutor // Return the hash password return strtolower(sha1($value)); } + + public function isPasswordEncrypted(mixed $password): bool + { + return (is_string($password) && strlen($password) === 40); + } } diff --git a/src/Model/UserModel.php b/src/Model/UserModel.php index d9f9544..e9e5c81 100644 --- a/src/Model/UserModel.php +++ b/src/Model/UserModel.php @@ -6,7 +6,7 @@ use ByJG\Authenticate\MapperFunctions\PasswordSha1Mapper; use ByJG\MicroOrm\Attributes\FieldAttribute; use ByJG\MicroOrm\Attributes\TableAttribute; -use ByJG\MicroOrm\Literal\HexUuidLiteral; +use ByJG\MicroOrm\Literal\Literal; use ByJG\MicroOrm\Trait\CreatedAt; use ByJG\MicroOrm\Trait\DeletedAt; use ByJG\MicroOrm\Trait\UpdatedAt; @@ -19,7 +19,7 @@ class UserModel use UpdatedAt; use DeletedAt; #[FieldAttribute(primaryKey: true)] - protected string|int|HexUuidLiteral|null $userid = null; + protected string|int|Literal|null $userid = null; #[FieldAttribute] protected ?string $name = null; @@ -60,17 +60,17 @@ public function __construct(string $name = "", string $email = "", string $usern /** - * @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/UserPropertiesModel.php b/src/Model/UserPropertiesModel.php index 7c60eee..080dd6b 100644 --- a/src/Model/UserPropertiesModel.php +++ b/src/Model/UserPropertiesModel.php @@ -4,13 +4,13 @@ use ByJG\MicroOrm\Attributes\FieldAttribute; use ByJG\MicroOrm\Attributes\TableAttribute; -use ByJG\MicroOrm\Literal\HexUuidLiteral; +use ByJG\MicroOrm\Literal\Literal; #[TableAttribute(tableName: 'users_property')] class UserPropertiesModel { #[FieldAttribute] - protected string|int|HexUuidLiteral|null $userid = null; + protected string|int|Literal|null $userid = null; #[FieldAttribute(primaryKey: true)] protected ?string $id = null; @@ -34,17 +34,17 @@ public function __construct(string $name = "", string $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/Repository/UserPropertiesRepository.php b/src/Repository/UserPropertiesRepository.php index f116710..01e1024 100644 --- a/src/Repository/UserPropertiesRepository.php +++ b/src/Repository/UserPropertiesRepository.php @@ -11,7 +11,7 @@ use ByJG\MicroOrm\Exception\OrmInvalidFieldsException; use ByJG\MicroOrm\Exception\OrmModelInvalidException; use ByJG\MicroOrm\Exception\RepositoryReadOnlyException; -use ByJG\MicroOrm\Literal\HexUuidLiteral; +use ByJG\MicroOrm\Literal\Literal; use ByJG\MicroOrm\Mapper; use ByJG\MicroOrm\Query; use ByJG\MicroOrm\Repository; @@ -54,7 +54,7 @@ public function save(UserPropertiesModel $model): UserPropertiesModel /** * Get properties by user ID * - * @param string|HexUuidLiteral|int $userid + * @param string|Literal|int $userid * @return UserPropertiesModel[] * @throws DatabaseException * @throws DbDriverNotConnected @@ -62,7 +62,7 @@ public function save(UserPropertiesModel $model): UserPropertiesModel * @throws XmlUtilException * @throws \Psr\SimpleCache\InvalidArgumentException */ - public function getByUserId(string|HexUuidLiteral|int $userid): array + public function getByUserId(string|Literal|int $userid): array { $useridField = $this->mapper->getFieldMap('userid')->getFieldName(); $query = Query::getInstance() @@ -75,7 +75,7 @@ public function getByUserId(string|HexUuidLiteral|int $userid): array /** * Get specific property by user ID and name * - * @param string|HexUuidLiteral|int $userid + * @param string|Literal|int $userid * @param string $propertyName * @return UserPropertiesModel[] * @throws DatabaseException @@ -84,7 +84,7 @@ public function getByUserId(string|HexUuidLiteral|int $userid): array * @throws XmlUtilException * @throws \Psr\SimpleCache\InvalidArgumentException */ - public function getByUserIdAndName(string|HexUuidLiteral|int $userid, string $propertyName): array + public function getByUserIdAndName(string|Literal|int $userid, string $propertyName): array { $useridField = $this->mapper->getFieldMap('userid')->getFieldName(); $nameField = $this->mapper->getFieldMap('name')->getFieldName(); @@ -100,13 +100,13 @@ public function getByUserIdAndName(string|HexUuidLiteral|int $userid, string $pr /** * Delete all properties for a user * - * @param string|HexUuidLiteral|int $userid + * @param string|Literal|int $userid * @return void * @throws DatabaseException * @throws DbDriverNotConnected * @throws RepositoryReadOnlyException */ - public function deleteByUserId(string|HexUuidLiteral|int $userid): void + public function deleteByUserId(string|Literal|int $userid): void { $useridField = $this->mapper->getFieldMap('userid')->getFieldName(); @@ -120,7 +120,7 @@ public function deleteByUserId(string|HexUuidLiteral|int $userid): void /** * Delete specific property by user ID and name * - * @param string|HexUuidLiteral|int $userid + * @param string|Literal|int $userid * @param string $propertyName * @param string|null $value * @return void @@ -128,7 +128,7 @@ public function deleteByUserId(string|HexUuidLiteral|int $userid): void * @throws DbDriverNotConnected * @throws RepositoryReadOnlyException */ - public function deleteByUserIdAndName(string|HexUuidLiteral|int $userid, string $propertyName, ?string $value = null): void + public function deleteByUserIdAndName(string|Literal|int $userid, string $propertyName, ?string $value = null): void { $useridField = $this->mapper->getFieldMap('userid')->getFieldName(); $nameField = $this->mapper->getFieldMap('name')->getFieldName(); diff --git a/src/Repository/UsersRepository.php b/src/Repository/UsersRepository.php index afe2031..fe84c40 100644 --- a/src/Repository/UsersRepository.php +++ b/src/Repository/UsersRepository.php @@ -12,7 +12,7 @@ use ByJG\MicroOrm\Exception\OrmModelInvalidException; use ByJG\MicroOrm\Exception\RepositoryReadOnlyException; use ByJG\MicroOrm\Exception\UpdateConstraintException; -use ByJG\MicroOrm\Literal\HexUuidLiteral; +use ByJG\MicroOrm\Literal\Literal; use ByJG\MicroOrm\Mapper; use ByJG\MicroOrm\Query; use ByJG\MicroOrm\Repository; @@ -66,7 +66,7 @@ public function save(UserModel $model): UserModel /** * Get user by ID * - * @param string|HexUuidLiteral|int $userid + * @param string|Literal|int $userid * @return UserModel|null * @throws DatabaseException * @throws DbDriverNotConnected @@ -76,7 +76,7 @@ public function save(UserModel $model): UserModel * @throws XmlUtilException * @throws \Psr\SimpleCache\InvalidArgumentException */ - public function getById(string|HexUuidLiteral|int $userid): ?UserModel + public function getById(string|Literal|int $userid): ?UserModel { return $this->repository->get($userid); } @@ -85,7 +85,7 @@ public function getById(string|HexUuidLiteral|int $userid): ?UserModel * Get user by field value * * @param string $field Property name (e.g., 'username', 'email') - * @param string|HexUuidLiteral|int $value + * @param string|Literal|int $value * @return UserModel|null * @throws DatabaseException * @throws DbDriverNotConnected @@ -93,7 +93,7 @@ public function getById(string|HexUuidLiteral|int $userid): ?UserModel * @throws XmlUtilException * @throws \Psr\SimpleCache\InvalidArgumentException */ - public function getByField(string $field, string|HexUuidLiteral|int $value): ?UserModel + 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); @@ -126,11 +126,11 @@ public function getAll(): array /** * Delete user by ID * - * @param string|HexUuidLiteral|int $userid + * @param string|Literal|int $userid * @return void * @throws \Exception */ - public function deleteById(string|HexUuidLiteral|int $userid): void + public function deleteById(string|Literal|int $userid): void { $this->repository->delete($userid); } diff --git a/src/Service/UsersService.php b/src/Service/UsersService.php index 0d36e6e..24b50bf 100644 --- a/src/Service/UsersService.php +++ b/src/Service/UsersService.php @@ -16,7 +16,7 @@ use ByJG\Authenticate\Repository\UsersRepository; use ByJG\JwtWrapper\JwtWrapper; use ByJG\JwtWrapper\JwtWrapperException; -use ByJG\MicroOrm\Literal\HexUuidLiteral; +use ByJG\MicroOrm\Literal\Literal; use ByJG\MicroOrm\Query; use Exception; use InvalidArgumentException; @@ -161,7 +161,7 @@ protected function canAddUser(UserModel $model): bool * @inheritDoc */ #[\Override] - public function getById(string|HexUuidLiteral|int $userid): ?UserModel + public function getById(string|Literal|int $userid): ?UserModel { $user = $this->usersRepository->getById($userid); if ($user !== null) { @@ -238,7 +238,7 @@ public function removeByLogin(string $login): bool * @inheritDoc */ #[\Override] - public function removeById(string|HexUuidLiteral|int $userid): bool + public function removeById(string|Literal|int $userid): bool { try { // Delete properties first @@ -278,7 +278,7 @@ public function isValidUser(string $login, string $password): ?UserModel * @inheritDoc */ #[\Override] - public function hasProperty(string|int|HexUuidLiteral $userId, string $propertyName, ?string $value = null): bool + public function hasProperty(string|int|Literal $userId, string $propertyName, ?string $value = null): bool { $user = $this->getById($userId); @@ -303,7 +303,7 @@ public function hasProperty(string|int|HexUuidLiteral $userId, string $propertyN * @inheritDoc */ #[\Override] - public function getProperty(string|HexUuidLiteral|int $userId, string $propertyName): array|string|UserPropertiesModel|null + public function getProperty(string|Literal|int $userId, string $propertyName): array|string|UserPropertiesModel|null { $properties = $this->propertiesRepository->getByUserIdAndName($userId, $propertyName); @@ -327,7 +327,7 @@ public function getProperty(string|HexUuidLiteral|int $userId, string $propertyN * @inheritDoc */ #[\Override] - public function addProperty(string|HexUuidLiteral|int $userId, string $propertyName, ?string $value): bool + public function addProperty(string|Literal|int $userId, string $propertyName, ?string $value): bool { $user = $this->getById($userId); if (empty($user)) { @@ -351,7 +351,7 @@ public function addProperty(string|HexUuidLiteral|int $userId, string $propertyN * @inheritDoc */ #[\Override] - public function setProperty(string|HexUuidLiteral|int $userId, string $propertyName, ?string $value): bool + public function setProperty(string|Literal|int $userId, string $propertyName, ?string $value): bool { $properties = $this->propertiesRepository->getByUserIdAndName($userId, $propertyName); @@ -375,7 +375,7 @@ public function setProperty(string|HexUuidLiteral|int $userId, string $propertyN * @inheritDoc */ #[\Override] - public function removeProperty(string|HexUuidLiteral|int $userId, string $propertyName, ?string $value = null): bool + public function removeProperty(string|Literal|int $userId, string $propertyName, ?string $value = null): bool { $user = $this->getById($userId); if ($user !== null) { diff --git a/tests/CustomUserModel.php b/tests/CustomUserModel.php index 2cf8cf2..58f1767 100644 --- a/tests/CustomUserModel.php +++ b/tests/CustomUserModel.php @@ -13,7 +13,7 @@ class CustomUserModel extends UserModel { #[FieldAttribute(fieldName: 'myuserid', primaryKey: true)] - protected string|int|\ByJG\MicroOrm\Literal\HexUuidLiteral|null $userid = null; + protected string|int|\ByJG\MicroOrm\Literal\Literal|null $userid = null; #[FieldAttribute(fieldName: 'myname')] protected ?string $name = null; diff --git a/tests/CustomUserPropertiesModel.php b/tests/CustomUserPropertiesModel.php index da6aed2..5274d39 100644 --- a/tests/CustomUserPropertiesModel.php +++ b/tests/CustomUserPropertiesModel.php @@ -10,7 +10,7 @@ class CustomUserPropertiesModel extends UserPropertiesModel { #[FieldAttribute(fieldName: 'theiruserid')] - protected string|int|\ByJG\MicroOrm\Literal\HexUuidLiteral|null $userid = null; + protected string|int|\ByJG\MicroOrm\Literal\Literal|null $userid = null; #[FieldAttribute(fieldName: 'theirid', primaryKey: true)] protected ?string $id = null; diff --git a/tests/Fixture/MyUserModel.php b/tests/Fixture/MyUserModel.php index 192ed98..00bc041 100644 --- a/tests/Fixture/MyUserModel.php +++ b/tests/Fixture/MyUserModel.php @@ -6,7 +6,7 @@ use ByJG\Authenticate\Model\UserModel; use ByJG\MicroOrm\Attributes\FieldAttribute; use ByJG\MicroOrm\Attributes\TableAttribute; -use ByJG\MicroOrm\Literal\HexUuidLiteral; +use ByJG\MicroOrm\Literal\Literal; use ByJG\MicroOrm\MapperFunctions\NowUtcMapper; use ByJG\MicroOrm\MapperFunctions\ReadOnlyMapper; @@ -14,7 +14,7 @@ class MyUserModel extends UserModel { #[FieldAttribute(fieldName: 'myuserid', primaryKey: true)] - protected string|int|HexUuidLiteral|null $userid = null; + protected string|int|Literal|null $userid = null; #[FieldAttribute(fieldName: 'myname')] protected ?string $name = null; diff --git a/tests/Fixture/MyUserPropertiesModel.php b/tests/Fixture/MyUserPropertiesModel.php index 8cffe49..93d1bc8 100644 --- a/tests/Fixture/MyUserPropertiesModel.php +++ b/tests/Fixture/MyUserPropertiesModel.php @@ -10,7 +10,7 @@ class MyUserPropertiesModel extends UserPropertiesModel { #[FieldAttribute(fieldName: 'theiruserid')] - protected string|int|\ByJG\MicroOrm\Literal\HexUuidLiteral|null $userid = null; + protected string|int|\ByJG\MicroOrm\Literal\Literal|null $userid = null; #[FieldAttribute(fieldName: 'theirid', primaryKey: true)] protected ?string $id = null; diff --git a/tests/Fixture/UserModelMd5.php b/tests/Fixture/UserModelMd5.php index ce19bf0..37feb30 100644 --- a/tests/Fixture/UserModelMd5.php +++ b/tests/Fixture/UserModelMd5.php @@ -12,7 +12,7 @@ class UserModelMd5 extends UserModel { #[FieldAttribute(primaryKey: true)] - protected string|int|\ByJG\MicroOrm\Literal\HexUuidLiteral|null $userid = null; + protected string|int|\ByJG\MicroOrm\Literal\Literal|null $userid = null; #[FieldAttribute] protected ?string $name = null; From 16cd8051de689bcdce4ea271aec533b99e7fbfdf Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Sun, 16 Nov 2025 11:01:05 -0500 Subject: [PATCH 27/40] Introduce `PasswordMapperInterface` for password encryption handling. Update `PasswordMd5Mapper` and `PasswordSha1Mapper` to implement the interface. Refactor `UsersService` to validate password mappers and integrate with `PasswordDefinition`. Adjust tests and models for compatibility. --- src/Definition/PasswordDefinition.php | 14 ++++++++++++++ src/Interfaces/PasswordMapperInterface.php | 10 ++++++++++ src/MapperFunctions/PasswordSha1Mapper.php | 4 ++-- src/Service/UsersService.php | 11 +++++++++++ tests/Fixture/PasswordMd5Mapper.php | 11 ++++++++--- tests/Fixture/UserModelMd5.php | 3 ++- 6 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 src/Interfaces/PasswordMapperInterface.php diff --git a/src/Definition/PasswordDefinition.php b/src/Definition/PasswordDefinition.php index 61ad5b4..5697a47 100644 --- a/src/Definition/PasswordDefinition.php +++ b/src/Definition/PasswordDefinition.php @@ -2,6 +2,7 @@ namespace ByJG\Authenticate\Definition; +use ByJG\Authenticate\Interfaces\PasswordMapperInterface; use InvalidArgumentException; use Random\RandomException; @@ -18,6 +19,8 @@ class PasswordDefinition protected array $rules = []; + protected PasswordMapperInterface|string|null $passwordMapper = null; + public function __construct($rules = null) { $this->rules = [ @@ -194,4 +197,15 @@ public function generatePassword(int $extendSize = 0): string 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/Interfaces/PasswordMapperInterface.php b/src/Interfaces/PasswordMapperInterface.php new file mode 100644 index 0000000..fb56f1a --- /dev/null +++ b/src/Interfaces/PasswordMapperInterface.php @@ -0,0 +1,10 @@ +getFieldMap(User::Password->value)->getUpdateFunction(); + if (!is_subclass_of($passwordUpdateFunction, PasswordMapperInterface::class, true)) { + throw new InvalidArgumentException('Password update function must implement PasswordMapperInterface'); + } + + if ($this->passwordDefinition !== null) { + $this->passwordDefinition->setPasswordMapper($passwordUpdateFunction); + } + $propertyMapper = $propertiesRepository->getRepository()->getMapper(); $propertyCheck = ($propertyMapper->getFieldMap(UserProperty::Userid->value) !== null) && ($propertyMapper->getFieldMap(UserProperty::Name->value) !== null) && diff --git a/tests/Fixture/PasswordMd5Mapper.php b/tests/Fixture/PasswordMd5Mapper.php index dde5a50..fed7be2 100644 --- a/tests/Fixture/PasswordMd5Mapper.php +++ b/tests/Fixture/PasswordMd5Mapper.php @@ -2,19 +2,19 @@ namespace Tests\Fixture; -use ByJG\MicroOrm\Interface\MapperFunctionInterface; +use ByJG\Authenticate\Interfaces\PasswordMapperInterface; /** * Custom MD5 Password Mapper for testing */ -class PasswordMd5Mapper implements MapperFunctionInterface +class PasswordMd5Mapper implements PasswordMapperInterface { #[\Override] public function processedValue(mixed $value, mixed $instance, mixed $executor = null): mixed { // Already have an MD5 hash (32 characters) - if (is_string($value) && strlen($value) === 32 && ctype_xdigit($value)) { + if ($this->isPasswordEncrypted($value)) { return $value; } @@ -26,4 +26,9 @@ public function processedValue(mixed $value, mixed $instance, mixed $executor = // Return the MD5 hash return strtolower(md5($value)); } + + public function isPasswordEncrypted(mixed $password): bool + { + return is_string($password) && strlen($password) === 32 && ctype_xdigit($password); + } } diff --git a/tests/Fixture/UserModelMd5.php b/tests/Fixture/UserModelMd5.php index 37feb30..ed2ef98 100644 --- a/tests/Fixture/UserModelMd5.php +++ b/tests/Fixture/UserModelMd5.php @@ -5,6 +5,7 @@ use ByJG\Authenticate\Model\UserModel; use ByJG\MicroOrm\Attributes\FieldAttribute; use ByJG\MicroOrm\Attributes\TableAttribute; +use ByJG\MicroOrm\Literal\Literal; use ByJG\MicroOrm\MapperFunctions\NowUtcMapper; use ByJG\MicroOrm\MapperFunctions\ReadOnlyMapper; @@ -12,7 +13,7 @@ class UserModelMd5 extends UserModel { #[FieldAttribute(primaryKey: true)] - protected string|int|\ByJG\MicroOrm\Literal\Literal|null $userid = null; + protected string|int|Literal|null $userid = null; #[FieldAttribute] protected ?string $name = null; From d919fd65e62f5a3b89f9fd8462f10827e2db1b4b Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Sun, 16 Nov 2025 11:58:11 -0500 Subject: [PATCH 28/40] Refactor documentation and `UsersService`: replace string-based login field constants with `LoginField` enum, enhance JWT token payloads, streamline password handling, update examples, and improve field mapper documentation. --- README.md | 5 +- docs/authentication.md | 14 +- docs/custom-fields.md | 6 +- docs/database-storage.md | 117 +++++- docs/examples.md | 12 +- docs/getting-started.md | 5 +- docs/jwt-tokens.md | 35 +- docs/mappers.md | 456 +++++++-------------- docs/password-validation.md | 41 +- docs/session-context.md | 25 +- docs/user-management.md | 38 +- docs/user-properties.md | 10 +- src/Interfaces/UsersServiceInterface.php | 4 +- src/MapperFunctions/PasswordSha1Mapper.php | 1 + src/Service/UsersService.php | 63 ++- tests/Fixture/PasswordMd5Mapper.php | 1 + tests/TestUsersBase.php | 3 +- 17 files changed, 463 insertions(+), 373 deletions(-) diff --git a/README.md b/README.md index fc84d4b..1a8c726 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ See [Installation Guide](docs/installation.md) for detailed setup instructions a addUser('John Doe', 'johndoe', 'john@example.com', 'SecurePass123'); @@ -68,6 +69,8 @@ if ($authenticatedUser !== null) { } ``` +Set the third constructor argument to `LoginField::Email` if you prefer authenticating users by email instead of username. + See [Getting Started](docs/getting-started.md) for a complete introduction and [Examples](docs/examples.md) for more use cases. ## Features diff --git a/docs/authentication.md b/docs/authentication.md index 9344557..d6436c5 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -22,8 +22,9 @@ if ($user !== null) { ``` :::tip Login Field -The `isValidUser()` method uses the login field configured in your `UsersService` constructor. This can be either `UsersService::LOGIN_IS_EMAIL` or `UsersService::LOGIN_IS_USERNAME`. +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 @@ -56,7 +57,8 @@ use ByJG\JwtWrapper\JwtHashHmacSecret; use ByJG\JwtWrapper\JwtWrapper; // Create JWT wrapper -$jwtWrapper = new JwtWrapper('your-server.com', new JwtHashHmacSecret('your-secret-key')); +$jwtSecret = getenv('JWT_SECRET') ?: JwtWrapper::generateSecret(64); // base64 encoded secret +$jwtWrapper = new JwtWrapper('your-server.com', new JwtHashHmacSecret($jwtSecret)); // Create authentication token $token = $users->createAuthToken( @@ -73,6 +75,8 @@ if ($token !== null) { } ``` +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. + ### Validating JWT Tokens ```php @@ -96,11 +100,9 @@ When a JWT token is created, a hash of the token is stored in the user's propert 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 (Legacy) +## Session-Based Authentication -:::warning Deprecated -SessionContext relies on traditional PHP sessions and is less secure than JWT tokens. It's maintained for backward compatibility only. **For new projects, use JWT tokens instead.** -::: +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 diff --git a/docs/custom-fields.md b/docs/custom-fields.md index 39e49ab..845b30d 100644 --- a/docs/custom-fields.md +++ b/docs/custom-fields.md @@ -124,6 +124,7 @@ CREATE TABLE users ```php +MySQL / MariaDB ```sql CREATE TABLE users @@ -41,6 +44,104 @@ CREATE TABLE users_property ) 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, + updated_at DATETIME, + deleted_at DATETIME, + role VARCHAR(20) +); + +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, + updated_at TIMESTAMP, + deleted_at TIMESTAMP, + role VARCHAR(20), + + CONSTRAINT pk_users PRIMARY KEY (userid) +); + +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, + updated_at DATETIME, + deleted_at DATETIME, + role VARCHAR(20), + + CONSTRAINT pk_users PRIMARY KEY (userid) +); + +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 @@ -49,6 +150,7 @@ CREATE TABLE users_property addUser('John Doe', 'johndoe', 'john@example.com', 'password123'); diff --git a/docs/examples.md b/docs/examples.md index 77a1e0e..4459264 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -16,6 +16,7 @@ This page contains complete, working examples for common use cases. // config.php require_once 'vendor/autoload.php'; +use ByJG\Authenticate\Enum\LoginField; use ByJG\Authenticate\Service\UsersService; use ByJG\Authenticate\Repository\UsersRepository; use ByJG\Authenticate\Repository\UserPropertiesRepository; @@ -35,7 +36,7 @@ $usersRepo = new UsersRepository($db, UserModel::class); $propsRepo = new UserPropertiesRepository($db, UserPropertiesModel::class); // Initialize user service -$users = new UsersService($usersRepo, $propsRepo, UsersService::LOGIN_IS_USERNAME); +$users = new UsersService($usersRepo, $propsRepo, LoginField::Username); // Initialize session $sessionContext = new SessionContext(Factory::createSessionPool()); @@ -263,6 +264,7 @@ exit; // api-config.php require_once 'vendor/autoload.php'; +use ByJG\Authenticate\Enum\LoginField; use ByJG\Authenticate\Service\UsersService; use ByJG\Authenticate\Repository\UsersRepository; use ByJG\Authenticate\Repository\UserPropertiesRepository; @@ -280,10 +282,11 @@ $db = DatabaseExecutor::using($dbDriver); // Initialize repositories and service $usersRepo = new UsersRepository($db, UserModel::class); $propsRepo = new UserPropertiesRepository($db, UserPropertiesModel::class); -$users = new UsersService($usersRepo, $propsRepo, UsersService::LOGIN_IS_USERNAME); +$users = new UsersService($usersRepo, $propsRepo, LoginField::Username); // JWT -$jwtWrapper = new JwtWrapper('api.example.com', new JwtHashHmacSecret(getenv('JWT_SECRET') ?: 'your-secret-key')); +$jwtSecret = getenv('JWT_SECRET') ?: 'base64-encoded-secret-goes-here=='; // Store this in environment variables +$jwtWrapper = new JwtWrapper('api.example.com', new JwtHashHmacSecret($jwtSecret)); // Helper function function jsonResponse($data, $statusCode = 200) @@ -415,6 +418,7 @@ try { // multi-tenant-example.php require_once 'vendor/autoload.php'; +use ByJG\Authenticate\Enum\LoginField; use ByJG\Authenticate\Service\UsersService; use ByJG\Authenticate\Repository\UsersRepository; use ByJG\Authenticate\Repository\UserPropertiesRepository; @@ -428,7 +432,7 @@ $db = DatabaseExecutor::using($dbDriver); $usersRepo = new UsersRepository($db, UserModel::class); $propsRepo = new UserPropertiesRepository($db, UserPropertiesModel::class); -$users = new UsersService($usersRepo, $propsRepo, UsersService::LOGIN_IS_USERNAME); +$users = new UsersService($usersRepo, $propsRepo, LoginField::Username); // Add user to organization function addUserToOrganization($users, $userId, $orgId, $role = 'member') diff --git a/docs/getting-started.md b/docs/getting-started.md index 6561ea5..540ee4e 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -13,6 +13,7 @@ Auth User PHP is a simple and customizable library for user authentication in PH addUser('John Doe', 'johndoe', 'john@example.com', 'SecurePass123'); @@ -39,6 +40,8 @@ if ($user !== null) { } ``` +To authenticate users by email instead of username, create the service with `LoginField::Email`. + ## Next Steps - [Installation](installation.md) - Install the library via Composer diff --git a/docs/jwt-tokens.md b/docs/jwt-tokens.md index 02cdf2d..827ff8e 100644 --- a/docs/jwt-tokens.md +++ b/docs/jwt-tokens.md @@ -22,16 +22,16 @@ JWT (JSON Web Tokens) is a compact, URL-safe means of representing claims to be ```php createAuthToken( ); ``` +### 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, + [], + [], + [User::Name, User::Email, 'department'] +); +``` + +In the example above, the token payload receives the user's `name`, `email`, and the value returned by `$user->get('department')` automatically. + ## Validating JWT Tokens ### Token Validation @@ -285,7 +306,7 @@ if (preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) { $jwtData = $jwtWrapper->extractData($token); $username = $jwtData->data['login'] ?? null; - $user = $users->get($username, $users->getUserDefinition()->loginField()); + $user = $users->getByLogin($username); if ($user !== null) { $users->removeProperty($user->getUserid(), 'TOKEN_HASH'); } diff --git a/docs/mappers.md b/docs/mappers.md index 5509eea..f881e3f 100644 --- a/docs/mappers.md +++ b/docs/mappers.md @@ -5,429 +5,265 @@ title: Mappers and Entity Processors # Mappers and Entity Processors -Mappers and Entity Processors allow you to transform data as it's read from or written to the database. +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. -## What Are Mappers? +## Controlling Fields with Attributes -Mappers implement the `MapperFunctionInterface` and transform individual field values during database operations. +`FieldAttribute` accepts optional mapper functions for each lifecycle event: -- **Update Mappers**: Transform values **before** saving to database -- **Select Mappers**: Transform values **after** reading from database - -## Built-in Mappers - -### PasswordSha1Mapper - -Automatically hashes passwords using SHA-1: +- `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 defineMapperForUpdate( - UserDefinition::FIELD_PASSWORD, - PasswordSha1Mapper::class -); +#[TableAttribute(tableName: 'users')] +class CustomUserModel extends UserModel +{ + // Store the column as full_name and prevent updates + #[FieldAttribute(fieldName: 'full_name', updateFunction: ReadOnlyMapper::class)] + protected ?string $name = null; + + // Normalize the email before persisting + #[FieldAttribute( + updateFunction: new ClosureMapper(fn ($value) => strtolower(trim((string) $value))), + selectFunction: StandardMapper::class + )] + protected ?string $email = null; +} ``` -### StandardMapper +### Built-in Mapper Helpers -Default mapper that passes values through unchanged: +This package ships with a few mapper utilities that complement the ones provided by Micro ORM: -```php -defineMapperForUpdate('name', StandardMapper::class); -``` + class UsernameAsIdModel extends UserModel + { + #[FieldAttribute(primaryKey: true, updateFunction: UserIdGeneratorMapper::class)] + protected string|int|null $userid = null; + } + ``` +- **ClosureMapper** – wraps anonymous functions so they implement `MapperFunctionInterface`. This is handy when you need a small transformation in place. + ```php + markPropertyAsReadOnly(UserDefinition::FIELD_CREATED); +#[FieldAttribute(fieldName: 'created_at', updateFunction: ReadOnlyMapper::class, insertFunction: NowUtcMapper::class)] +protected ?string $createdAt = null; -// Or explicitly -$userDefinition->defineMapperForUpdate( - UserDefinition::FIELD_CREATED, - ReadOnlyMapper::class -); +#[FieldAttribute(fieldName: 'updated_at', updateFunction: NowUtcMapper::class)] +protected ?string $updatedAt = null; ``` ## Creating Custom Mappers -### Mapper Interface +Any mapper only needs to implement `MapperFunctionInterface`: ```php 12]); + return password_hash((string) $value, PASSWORD_BCRYPT, ['cost' => 12]); } } - -// Use it -$userDefinition->defineMapperForUpdate( - UserDefinition::FIELD_PASSWORD, - BcryptPasswordMapper::class -); ``` -### Example: Email Normalization Mapper +Attach it to the password field: ```php defineMapperForUpdate( - UserDefinition::FIELD_EMAIL, - EmailNormalizationMapper::class -); +#[FieldAttribute(updateFunction: BcryptPasswordMapper::class)] +protected ?string $password = null; ``` -### Example: JSON Serialization Mappers +### JSON Columns Example ```php defineMapperForUpdate('preferences', JsonEncodeMapper::class); -$userDefinition->defineMapperForSelect('preferences', JsonDecodeMapper::class); -``` - -### Example: Date Formatting Mapper - -```php -format('Y-m-d H:i:s'); - } - if (is_string($value)) { - return $value; - } - if (is_int($value)) { - return date('Y-m-d H:i:s', $value); - } - return $value; - } -} - -class DateParseMapper implements MapperFunctionInterface -{ - public function processedValue(mixed $value, mixed $instance): mixed - { - if (empty($value)) { - return null; - } - try { - return new \DateTime($value); - } catch (\Exception $e) { - return $value; - } - } -} - -$userDefinition->defineMapperForUpdate('created', DateFormatMapper::class); -$userDefinition->defineMapperForSelect('created', DateParseMapper::class); +#[FieldAttribute( + fieldName: 'preferences', + updateFunction: JsonEncodeMapper::class, + selectFunction: JsonDecodeMapper::class +)] +protected array $preferences = []; ``` ## Entity Processors -Entity Processors transform the **entire entity** (UserModel) before insert or update operations. - -### Entity Processor Interface - -```php -setBeforeInsert(new PassThroughEntityProcessor()); -``` - -### Custom Entity Processors - -#### Example: Auto-Set Created Timestamp - -```php -getCreatedAt())) { - $instance->setCreatedAt(date('Y-m-d H:i:s')); - } - } - } -} - -$userDefinition->setBeforeInsert(new CreatedTimestampProcessor()); -``` - -#### Example: Username Validation - -```php -getUsername(); + if (!$instance instanceof UserModel) { + return; + } - if (strlen($username) < 3) { - throw new \InvalidArgumentException('Username must be at least 3 characters'); - } + $username = (string) $instance->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'); - } + if (!preg_match('/^[A-Za-z0-9_]+$/', $username)) { + throw new \InvalidArgumentException('Username can only contain letters, numbers, and underscores.'); } } } -$userDefinition->setBeforeInsert(new UsernameValidationProcessor()); -$userDefinition->setBeforeUpdate(new UsernameValidationProcessor()); -``` - -#### Example: Audit Trail - -```php -userId = $userId; } public function process(mixed $instance): void { if ($instance instanceof UserModel) { - $instance->set('modified_by', $this->userId); + $instance->set('modified_by', (string) $this->actorId); $instance->set('modified_at', date('Y-m-d H:i:s')); } } } - -$userDefinition->setBeforeUpdate(new AuditProcessor($currentUserId)); ``` -## Using Closures (Legacy) - -For backward compatibility, you can use closures instead of dedicated mapper classes: +Attach them using the table attribute: ```php defineMapperForUpdate( - UserDefinition::FIELD_EMAIL, - new ClosureMapper(function ($value, $instance) { - return strtolower(trim($value)); - }) -); - -// Select mapper -$userDefinition->defineMapperForSelect( - UserDefinition::FIELD_CREATED, - new ClosureMapper(function ($value, $instance) { - return date('Y', strtotime($value)); - }) -); +use App\Processor\AuditProcessor; +use App\Processor\UsernameValidationProcessor; +use ByJG\MicroOrm\Attributes\TableAttribute; + +#[TableAttribute( + tableName: 'users', + beforeInsert: UsernameValidationProcessor::class, + beforeUpdate: AuditProcessor::class +)] +class ProcessedUserModel extends CustomUserModel +{ +} ``` -:::warning Deprecated Methods -The following methods are deprecated but still work: -- `defineClosureForUpdate()` - Use `defineMapperForUpdate()` with `ClosureMapper` -- `defineClosureForSelect()` - Use `defineMapperForSelect()` with `ClosureMapper` -- `getClosureForUpdate()` - Use `getMapperForUpdate()` -- `getClosureForSelect()` - Use `getMapperForSelect()` -::: +If you need runtime dependencies (like the current actor ID), instantiate the processor and configure it on the repository: + +```php +getUsersRepository()->getRepository(); +$repository->setBeforeUpdate(new AuditProcessor($currentUserId)); +``` ## Complete Example ```php getCreatedAt())) { - $instance->setCreatedAt(date('Y-m-d H:i:s')); - } - if (empty($instance->getRole())) { - $instance->setRole('user'); - } - } - } -} - -// Configure User Definition -$userDefinition = new UserDefinition(); + #[FieldAttribute(fieldName: 'created_at', updateFunction: ReadOnlyMapper::class, insertFunction: NowUtcMapper::class)] + protected ?string $createdAt = null; -// Apply mappers -$userDefinition->defineMapperForUpdate('name', TrimMapper::class); -$userDefinition->defineMapperForUpdate('email', LowercaseMapper::class); -$userDefinition->defineMapperForUpdate('username', LowercaseMapper::class); + #[FieldAttribute(fieldName: 'updated_at', updateFunction: NowUtcMapper::class)] + protected ?string $updatedAt = null; -// Apply entity processors -$userDefinition->setBeforeInsert(new DefaultsProcessor()); + #[FieldAttribute(updateFunction: new ClosureMapper(fn ($value) => strtolower(trim((string) $value))))] + protected ?string $email = null; +} -// Initialize -$users = new UsersDBDataset($dbDriver, $userDefinition); +$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); ``` -## Best Practices - -1. **Keep mappers simple** - Each mapper should do one thing -2. **Chain mappers** - Use composition for complex transformations -3. **Handle null values** - Always check for null/empty values -4. **Be idempotent** - Applying mapper multiple times should be safe -5. **Use entity processors for validation** - Validate complete entities -6. **Document side effects** - Make it clear what each mapper does - -## Next Steps - -- [Custom Fields](custom-fields.md) - Extending UserModel -- [Password Validation](password-validation.md) - Password policies -- [Database Storage](database-storage.md) - Schema configuration +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 index 1306ac4..3082d2f 100644 --- a/docs/password-validation.md +++ b/docs/password-validation.md @@ -27,6 +27,45 @@ $userModel->withPasswordDefinition($passwordDefinition); $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 @@ -253,7 +292,7 @@ Repeated patterns include: get($userId); + $user = $users->getById($userId); $user->withPasswordDefinition($passwordDefinition); // Verify old password diff --git a/docs/session-context.md b/docs/session-context.md index d361105..697c4fe 100644 --- a/docs/session-context.md +++ b/docs/session-context.md @@ -66,10 +66,8 @@ $sessionContext = new SessionContext($cachePool, $uniquePrefix); registerLogin($userId); - -// With additional session data -$sessionContext->registerLogin($userId, ['ip' => $_SERVER['REMOTE_ADDR']]); ``` +Call `setSessionData()` after `registerLogin()` if you need to store extra session metadata (e.g., IP address, login time). ### Check Authentication Status @@ -142,12 +140,25 @@ Returns `false` if: ```php isAuthenticated()) { } $userId = $sessionContext->userInfo(); -$user = $users->get($userId); +$user = $users->getById($userId); $loginTime = $sessionContext->getSessionData('login_time'); echo "Welcome, " . $user->getName(); diff --git a/docs/user-management.md b/docs/user-management.md index 9a94d2b..c100d4e 100644 --- a/docs/user-management.md +++ b/docs/user-management.md @@ -39,6 +39,23 @@ $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: @@ -153,15 +170,18 @@ The `hasRole()` method performs case-insensitive comparison, so `hasRole('admin' The `UserModel` class provides the following properties: -| Property | Type | Description | -|------------|---------------------|--------------------------------| -| userid | string\|int\|null | User ID (auto-generated) | -| name | string\|null | User's full name | -| email | string\|null | User's email address | -| username | string\|null | User's username | -| password | string\|null | User's password (hashed) | -| created | string\|null | Creation timestamp | -| role | string\|null | User's role (admin, moderator, user, etc.) | +| 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 diff --git a/docs/user-properties.md b/docs/user-properties.md index ff6f4dd..e721387 100644 --- a/docs/user-properties.md +++ b/docs/user-properties.md @@ -49,7 +49,7 @@ You can also manage properties directly through the `UserModel`: get($userId); +$user = $users->getById($userId); // Set a property value $user->set('phone', '555-1234'); @@ -88,7 +88,7 @@ Returns `null` if the property doesn't exist. ```php get($userId); +$user = $users->getById($userId); // Get property value(s) $phone = $user->get('phone'); @@ -120,10 +120,6 @@ if ($users->hasProperty($userId, 'role', 'admin')) { } ``` -:::tip Admin Bypass -The `hasProperty()` method always returns `true` for admin users, regardless of the actual property values. -::: - ## Removing Properties ### Remove a Specific Property Value @@ -167,7 +163,7 @@ $engineers = $users->getUsersByProperty('department', 'Engineering'); ```php getUsersByPropertySet([ +$matchingUsers = $users->getUsersByPropertySet([ 'department' => 'Engineering', 'role' => 'senior', 'status' => 'active' diff --git a/src/Interfaces/UsersServiceInterface.php b/src/Interfaces/UsersServiceInterface.php index e86fcd8..0ad33e6 100644 --- a/src/Interfaces/UsersServiceInterface.php +++ b/src/Interfaces/UsersServiceInterface.php @@ -172,6 +172,7 @@ public function getUsersByPropertySet(array $propertiesArray): array; * @param int $expires * @param array $updateUserInfo * @param array $updateTokenInfo + * @param array $tokenUserFields * @return string|null */ public function createAuthToken( @@ -180,7 +181,8 @@ public function createAuthToken( JwtWrapper $jwtWrapper, int $expires = 1200, array $updateUserInfo = [], - array $updateTokenInfo = [] + array $updateTokenInfo = [], + array $tokenUserFields = [] ): ?string; /** diff --git a/src/MapperFunctions/PasswordSha1Mapper.php b/src/MapperFunctions/PasswordSha1Mapper.php index 53e7a49..db3b79d 100644 --- a/src/MapperFunctions/PasswordSha1Mapper.php +++ b/src/MapperFunctions/PasswordSha1Mapper.php @@ -27,6 +27,7 @@ public function processedValue(mixed $value, mixed $instance, ?DatabaseExecutor return strtolower(sha1($value)); } + #[\Override] public function isPasswordEncrypted(mixed $password): bool { return (is_string($password) && strlen($password) === 40); diff --git a/src/Service/UsersService.php b/src/Service/UsersService.php index ba21ea1..daf8192 100644 --- a/src/Service/UsersService.php +++ b/src/Service/UsersService.php @@ -62,9 +62,7 @@ public function __construct( throw new InvalidArgumentException('Password update function must implement PasswordMapperInterface'); } - if ($this->passwordDefinition !== null) { - $this->passwordDefinition->setPasswordMapper($passwordUpdateFunction); - } + $this->passwordDefinition?->setPasswordMapper($passwordUpdateFunction); $propertyMapper = $propertiesRepository->getRepository()->getMapper(); $propertyCheck = ($propertyMapper->getFieldMap(UserProperty::Userid->value) !== null) && @@ -140,9 +138,7 @@ public function addUser(string $name, string $userName, string $email, string $p $mapper->getFieldMap(User::Email->value)->getFieldName() => $email, $mapper->getFieldMap(User::Username->value)->getFieldName() => $userName, ]); - if ($this->passwordDefinition !== null) { - $model->withPasswordDefinition($this->passwordDefinition); - } + $this->applyPasswordDefinition($model); $model->setPassword($password); return $this->save($model); @@ -177,6 +173,7 @@ public function getById(string|Literal|int $userid): ?UserModel $user = $this->usersRepository->getById($userid); if ($user !== null) { $this->loadUserProperties($user); + $this->applyPasswordDefinition($user); } return $user; } @@ -191,6 +188,7 @@ public function getByEmail(string $email): ?UserModel $user = $this->usersRepository->getByField($fieldMap->getFieldName(), $email); if ($user !== null) { $this->loadUserProperties($user); + $this->applyPasswordDefinition($user); } return $user; } @@ -205,6 +203,7 @@ public function getByUsername(string $username): ?UserModel $user = $this->usersRepository->getByField($fieldMap->getFieldName(), $username); if ($user !== null) { $this->loadUserProperties($user); + $this->applyPasswordDefinition($user); } return $user; } @@ -232,6 +231,29 @@ protected function loadUserProperties(UserModel $user): void $user->setProperties($properties); } + protected function applyPasswordDefinition(UserModel $user): void + { + if ($this->passwordDefinition !== null) { + $user->withPasswordDefinition($this->passwordDefinition); + } + } + + protected function resolveUserFieldValue(UserModel $user, string $fieldName): mixed + { + $camel = str_replace(' ', '', ucwords(str_replace(['_', '-'], ' ' , $fieldName))); + $method = 'get' . $camel; + if (method_exists($user, $method)) { + return $user->$method(); + } + + $value = $user->get($fieldName); + if ($value !== null) { + return $value; + } + + return null; + } + /** * @inheritDoc */ @@ -445,7 +467,13 @@ public function getUsersByPropertySet(array $propertiesArray): array ->where("p$count.$propValueField = :value$count", ["value$count" => $value]); } - return $this->usersRepository->getRepository()->getByQuery($query); + $users = $this->usersRepository->getRepository()->getByQuery($query); + if ($this->passwordDefinition !== null) { + foreach ($users as $user) { + $this->applyPasswordDefinition($user); + } + } + return $users; } /** @@ -458,7 +486,8 @@ public function createAuthToken( JwtWrapper $jwtWrapper, int $expires = 1200, array $updateUserInfo = [], - array $updateTokenInfo = [] + array $updateTokenInfo = [], + array $tokenUserFields = [User::Userid, User::Name, User::Role] ): ?string { $user = $this->isValidUser($login, $password); if (is_null($user)) { @@ -469,8 +498,18 @@ public function createAuthToken( $user->set($key, $value); } - $updateTokenInfo['login'] = $login; - $updateTokenInfo['userid'] = $user->getUserid(); + foreach ($tokenUserFields as $field) { + $fieldName = $field instanceof User ? $field->value : $field; + if (!is_string($fieldName) || $fieldName === '') { + continue; + } + + $value = $this->resolveUserFieldValue($user, $fieldName); + if ($value !== null) { + $updateTokenInfo[$fieldName] = $value; + } + } + $jwtData = $jwtWrapper->createJwtData($updateTokenInfo, $expires); $token = $jwtWrapper->generateToken($jwtData); @@ -513,6 +552,8 @@ public function isValidToken(string $login, JwtWrapper $jwtWrapper, string $toke #[\Override] public function getUsersEntity(array $fields): UserModel { - return $this->getUsersRepository()->getMapper()->getEntity($fields); + $entity = $this->getUsersRepository()->getMapper()->getEntity($fields); + $this->applyPasswordDefinition($entity); + return $entity; } } diff --git a/tests/Fixture/PasswordMd5Mapper.php b/tests/Fixture/PasswordMd5Mapper.php index fed7be2..e845549 100644 --- a/tests/Fixture/PasswordMd5Mapper.php +++ b/tests/Fixture/PasswordMd5Mapper.php @@ -27,6 +27,7 @@ public function processedValue(mixed $value, mixed $instance, mixed $executor = 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/TestUsersBase.php b/tests/TestUsersBase.php index 08d089b..53907a1 100644 --- a/tests/TestUsersBase.php +++ b/tests/TestUsersBase.php @@ -214,8 +214,9 @@ protected function expectedToken(string $tokenData, string $login, int $userId): $dataFromToken = new \stdClass(); $dataFromToken->tokenData = $tokenData; - $dataFromToken->login = $loginCreated; $dataFromToken->userid = $userId; + $dataFromToken->name = $user->getName(); + $dataFromToken->role = $user->getRole(); $tokenResult = $this->object->isValidToken($loginCreated, $jwtWrapper, $token); From 79e07c48308d77db46491b61ff0af64373805201 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Sun, 16 Nov 2025 12:19:11 -0500 Subject: [PATCH 29/40] Refactor documentation and `UsersService`: replace string-based login field constants with `LoginField` enum, enhance JWT token payloads, streamline password handling, update examples, and improve field mapper documentation. --- docs/database-storage.md | 78 +++++++++++++++++++++++++++++++++------- 1 file changed, 66 insertions(+), 12 deletions(-) diff --git a/docs/database-storage.md b/docs/database-storage.md index d81db5f..1ffbcfb 100644 --- a/docs/database-storage.md +++ b/docs/database-storage.md @@ -24,12 +24,16 @@ CREATE TABLE users email VARCHAR(120), username VARCHAR(15) NOT NULL, password CHAR(40) NOT NULL, - created_at DATETIME, - updated_at DATETIME, + 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 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 @@ -57,12 +61,25 @@ CREATE TABLE users email VARCHAR(120), username VARCHAR(15) NOT NULL, password CHAR(40) NOT NULL, - created_at DATETIME, - updated_at DATETIME, + created_at DATETIME DEFAULT (datetime('now')), + updated_at DATETIME DEFAULT (datetime('now')), deleted_at DATETIME, - role VARCHAR(20) + 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, @@ -87,14 +104,32 @@ CREATE TABLE users email VARCHAR(120), username VARCHAR(15) NOT NULL, password CHAR(40) NOT NULL, - created_at TIMESTAMP, - updated_at TIMESTAMP, + created_at TIMESTAMP DEFAULT now(), + updated_at TIMESTAMP DEFAULT now(), deleted_at TIMESTAMP, role VARCHAR(20), - CONSTRAINT pk_users PRIMARY KEY (userid) + 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, @@ -120,14 +155,33 @@ CREATE TABLE users email VARCHAR(120), username VARCHAR(15) NOT NULL, password CHAR(40) NOT NULL, - created_at DATETIME, - updated_at DATETIME, + created_at DATETIME DEFAULT GETDATE(), + updated_at DATETIME DEFAULT GETDATE(), deleted_at DATETIME, role VARCHAR(20), - CONSTRAINT pk_users PRIMARY KEY (userid) + 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, From b40ec87f512a3255439c41e98aae96983a3ed968 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Sun, 16 Nov 2025 13:02:54 -0500 Subject: [PATCH 30/40] Refactor `UsersService` to return `UserToken` instead of raw strings or arrays for token-related methods. Update documentation, examples, interface definitions, tests, and dependencies accordingly to enhance consistency and clarity. --- docs/authentication.md | 14 ++++---- docs/examples.md | 12 +++---- docs/jwt-tokens.md | 43 +++++++++++++----------- src/Interfaces/UsersServiceInterface.php | 9 ++--- src/Model/UserToken.php | 17 ++++++++++ src/Service/UsersService.php | 21 ++++++++---- tests/TestUsersBase.php | 33 +++++++++--------- 7 files changed, 89 insertions(+), 60 deletions(-) create mode 100644 src/Model/UserToken.php diff --git a/docs/authentication.md b/docs/authentication.md index d6436c5..4c7793f 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -61,7 +61,7 @@ $jwtSecret = getenv('JWT_SECRET') ?: JwtWrapper::generateSecret(64); // base64 $jwtWrapper = new JwtWrapper('your-server.com', new JwtHashHmacSecret($jwtSecret)); // Create authentication token -$token = $users->createAuthToken( +$userToken = $users->createAuthToken( 'johndoe', // Login 'SecurePass123', // Password $jwtWrapper, @@ -70,8 +70,8 @@ $token = $users->createAuthToken( ['role' => 'admin'] // Additional token data ); -if ($token !== null) { - echo "Token: " . $token; +if ($userToken !== null) { + echo "Token: " . $userToken->token; } ``` @@ -81,11 +81,11 @@ Need to include standard user columns (name, email, etc.) automatically? Pass th ```php isValidToken('johndoe', $jwtWrapper, $token); +$userToken = $users->isValidToken('johndoe', $jwtWrapper, $token); -if ($result !== null) { - $user = $result['user']; - $tokenData = $result['data']; +if ($userToken !== null) { + $user = $userToken->user; + $tokenData = $userToken->data; echo "User: " . $user->getName(); echo "Role: " . $tokenData['role']; diff --git a/docs/examples.md b/docs/examples.md index 4459264..bf82345 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -314,7 +314,7 @@ $username = $input['username'] ?? ''; $password = $input['password'] ?? ''; try { - $token = $users->createAuthToken( + $userToken = $users->createAuthToken( $username, $password, $jwtWrapper, @@ -328,13 +328,13 @@ try { ] ); - if ($token === null) { + if ($userToken === null) { jsonResponse(['error' => 'Invalid credentials'], 401); } jsonResponse([ 'success' => true, - 'token' => $token, + 'token' => $userToken->token, 'expires_in' => 3600 ]); @@ -370,13 +370,13 @@ try { } // Validate token - $result = $users->isValidToken($username, $jwtWrapper, $token); + $userToken = $users->isValidToken($username, $jwtWrapper, $token); - if ($result === null) { + if ($userToken === null) { jsonResponse(['error' => 'Token validation failed'], 401); } - $user = $result['user']; + $user = $userToken->user; // Handle request if ($_SERVER['REQUEST_METHOD'] === 'GET') { diff --git a/docs/jwt-tokens.md b/docs/jwt-tokens.md index 827ff8e..1499608 100644 --- a/docs/jwt-tokens.md +++ b/docs/jwt-tokens.md @@ -45,16 +45,16 @@ Use your API hostname (or any issuer string you want to validate) as the first a ```php createAuthToken( +$userToken = $users->createAuthToken( 'johndoe', // Login (username or email) 'password123', // Password $jwtWrapper, // JWT wrapper instance 3600 // Expires in 1 hour (seconds) ); -if ($token !== null) { +if ($userToken !== null) { // Return token to client - echo json_encode(['token' => $token]); + echo json_encode(['token' => $userToken->token]); } else { // Authentication failed http_response_code(401); @@ -68,7 +68,7 @@ You can include additional data in the JWT payload: ```php createAuthToken( +$userToken = $users->createAuthToken( 'johndoe', 'password123', $jwtWrapper, @@ -80,13 +80,16 @@ $token = $users->createAuthToken( 'tenant_id' => '12345' ] ); + +// Access the token string +$token = $userToken->token; ``` ### Update User Properties on Login ```php createAuthToken( +$userToken = $users->createAuthToken( 'johndoe', 'password123', $jwtWrapper, @@ -109,7 +112,7 @@ Instead of manually adding every field to `$updateTokenInfo`, pass a seventh arg createAuthToken( +$userToken = $users->createAuthToken( 'johndoe', 'password123', $jwtWrapper, @@ -129,11 +132,11 @@ In the example above, the token payload receives the user's `name`, `email`, and ```php isValidToken('johndoe', $jwtWrapper, $token); + $userToken = $users->isValidToken('johndoe', $jwtWrapper, $token); - if ($result !== null) { - $user = $result['user']; // UserModel instance - $tokenData = $result['data']; // Token payload data + if ($userToken !== null) { + $user = $userToken->user; // UserModel instance + $tokenData = $userToken->data; // Token payload data echo "Authenticated: " . $user->getName(); echo "Role: " . $tokenData['role']; @@ -202,7 +205,7 @@ header('Content-Type: application/json'); $input = json_decode(file_get_contents('php://input'), true); try { - $token = $users->createAuthToken( + $userToken = $users->createAuthToken( $input['username'], $input['password'], $jwtWrapper, @@ -217,13 +220,13 @@ try { ] ); - if ($token === null) { + if ($userToken === null) { throw new Exception('Authentication failed'); } echo json_encode([ 'success' => true, - 'token' => $token, + 'token' => $userToken->token, 'expires_in' => 3600 ]); @@ -265,13 +268,13 @@ try { } // Validate token - $result = $users->isValidToken($username, $jwtWrapper, $token); + $userToken = $users->isValidToken($username, $jwtWrapper, $token); - if ($result === null) { + if ($userToken === null) { throw new Exception('Invalid token'); } - $user = $result['user']; + $user = $userToken->user; // Process request echo json_encode([ @@ -348,7 +351,7 @@ For long-lived sessions, implement a refresh token pattern: ```php createAuthToken( +$accessUserToken = $users->createAuthToken( $login, $password, $jwtWrapper, @@ -358,7 +361,7 @@ $accessToken = $users->createAuthToken( ); // Create long-lived refresh token -$refreshToken = $users->createAuthToken( +$refreshUserToken = $users->createAuthToken( $login, $password, $jwtWrapperRefresh, // Different wrapper/key @@ -368,8 +371,8 @@ $refreshToken = $users->createAuthToken( ); echo json_encode([ - 'access_token' => $accessToken, - 'refresh_token' => $refreshToken + 'access_token' => $accessUserToken->token, + 'refresh_token' => $refreshUserToken->token ]); ``` diff --git a/src/Interfaces/UsersServiceInterface.php b/src/Interfaces/UsersServiceInterface.php index 0ad33e6..ca51a47 100644 --- a/src/Interfaces/UsersServiceInterface.php +++ b/src/Interfaces/UsersServiceInterface.php @@ -4,6 +4,7 @@ use ByJG\Authenticate\Model\UserModel; use ByJG\Authenticate\Model\UserPropertiesModel; +use ByJG\Authenticate\Model\UserToken; use ByJG\JwtWrapper\JwtWrapper; use ByJG\MicroOrm\Literal\Literal; @@ -173,7 +174,7 @@ public function getUsersByPropertySet(array $propertiesArray): array; * @param array $updateUserInfo * @param array $updateTokenInfo * @param array $tokenUserFields - * @return string|null + * @return UserToken|null */ public function createAuthToken( string $login, @@ -183,7 +184,7 @@ public function createAuthToken( array $updateUserInfo = [], array $updateTokenInfo = [], array $tokenUserFields = [] - ): ?string; + ): ?UserToken; /** * Validate authentication token @@ -191,9 +192,9 @@ public function createAuthToken( * @param string $login * @param JwtWrapper $jwtWrapper * @param string $token - * @return array|null + * @return UserToken */ - public function isValidToken(string $login, JwtWrapper $jwtWrapper, string $token): ?array; + public function isValidToken(string $login, JwtWrapper $jwtWrapper, string $token): UserToken; public function getUsersEntity(array $fields): UserModel; } 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/Service/UsersService.php b/src/Service/UsersService.php index daf8192..22b50ff 100644 --- a/src/Service/UsersService.php +++ b/src/Service/UsersService.php @@ -13,6 +13,7 @@ use ByJG\Authenticate\Interfaces\UsersServiceInterface; use ByJG\Authenticate\Model\UserModel; use ByJG\Authenticate\Model\UserPropertiesModel; +use ByJG\Authenticate\Model\UserToken; use ByJG\Authenticate\Repository\UserPropertiesRepository; use ByJG\Authenticate\Repository\UsersRepository; use ByJG\JwtWrapper\JwtWrapper; @@ -488,7 +489,8 @@ public function createAuthToken( array $updateUserInfo = [], array $updateTokenInfo = [], array $tokenUserFields = [User::Userid, User::Name, User::Role] - ): ?string { + ): ?UserToken + { $user = $this->isValidUser($login, $password); if (is_null($user)) { throw new UserNotFoundException('User not found'); @@ -517,7 +519,11 @@ public function createAuthToken( $user->set('TOKEN_HASH', sha1($token)); $this->save($user); - return $token; + return new UserToken( + user: $user, + token: $token, + data: $tokenUserFields + ); } /** @@ -527,7 +533,7 @@ public function createAuthToken( * @throws UserNotFoundException */ #[\Override] - public function isValidToken(string $login, JwtWrapper $jwtWrapper, string $token): ?array + public function isValidToken(string $login, JwtWrapper $jwtWrapper, string $token): UserToken { $user = $this->getByLogin($login); @@ -543,10 +549,11 @@ public function isValidToken(string $login, JwtWrapper $jwtWrapper, string $toke $this->save($user); - return [ - 'user' => $user, - 'data' => $data->data - ]; + return new UserToken( + user: $user, + token: $token, + data: (array)$data->data + ); } #[\Override] diff --git a/tests/TestUsersBase.php b/tests/TestUsersBase.php index 53907a1..cc0a25d 100644 --- a/tests/TestUsersBase.php +++ b/tests/TestUsersBase.php @@ -201,7 +201,7 @@ protected function expectedToken(string $tokenData, string $login, int $userId): $jwtWrapper = new JwtWrapper('api.test.com', new JwtHashHmacSecret('12345678', false)); - $token = $this->object->createAuthToken( + $userToken = $this->object->createAuthToken( $loginCreated, 'pwd2', $jwtWrapper, @@ -212,24 +212,25 @@ protected function expectedToken(string $tokenData, string $login, int $userId): $user = $this->object->getByLogin($login); - $dataFromToken = new \stdClass(); - $dataFromToken->tokenData = $tokenData; - $dataFromToken->userid = $userId; - $dataFromToken->name = $user->getName(); - $dataFromToken->role = $user->getRole(); + $dataFromToken = [ + 'tokenData' => $tokenData, + 'userid' => $userId, + 'name' => $user->getName(), + 'role' => $user->getRole(), + ]; - $tokenResult = $this->object->isValidToken($loginCreated, $jwtWrapper, $token); + $tokenResult = $this->object->isValidToken($loginCreated, $jwtWrapper, $userToken->token); // Compare data object - $this->assertEquals($dataFromToken, $tokenResult['data']); + $this->assertEquals($dataFromToken, $tokenResult->data); // Compare user fields (excluding timestamps which may differ) - $this->assertEquals($user->getUserid(), $tokenResult['user']->getUserid()); - $this->assertEquals($user->getName(), $tokenResult['user']->getName()); - $this->assertEquals($user->getEmail(), $tokenResult['user']->getEmail()); - $this->assertEquals($user->getUsername(), $tokenResult['user']->getUsername()); - $this->assertEquals($user->getPassword(), $tokenResult['user']->getPassword()); - $this->assertEquals($user->getRole(), $tokenResult['user']->getRole()); + $this->assertEquals($user->getUserid(), $tokenResult->user->getUserid()); + $this->assertEquals($user->getName(), $tokenResult->user->getName()); + $this->assertEquals($user->getEmail(), $tokenResult->user->getEmail()); + $this->assertEquals($user->getUsername(), $tokenResult->user->getUsername()); + $this->assertEquals($user->getPassword(), $tokenResult->user->getPassword()); + $this->assertEquals($user->getRole(), $tokenResult->user->getRole()); } /** @@ -244,7 +245,7 @@ public function testValidateTokenWithAnotherUser(): void $loginToFail = $this->__chooseValue('user1', 'user1@gmail.com'); $jwtWrapper = new JwtWrapper('api.test.com', new JwtHashHmacSecret('1234567')); - $token = $this->object->createAuthToken( + $userToken = $this->object->createAuthToken( $login, 'pwd2', $jwtWrapper, @@ -253,7 +254,7 @@ public function testValidateTokenWithAnotherUser(): void ['tokenData'=>'tokenValue'] ); - $this->object->isValidToken($loginToFail, $jwtWrapper, $token); + $this->object->isValidToken($loginToFail, $jwtWrapper, $userToken->token); } /** From ad1535a34de2ac8fae6eadf9d36ec312832ce4eb Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Sun, 16 Nov 2025 13:40:58 -0500 Subject: [PATCH 31/40] Update `UserPropertiesModel` constructor to allow nullable `name` and `value` parameters. --- src/Model/UserPropertiesModel.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Model/UserPropertiesModel.php b/src/Model/UserPropertiesModel.php index 080dd6b..cf2ad4f 100644 --- a/src/Model/UserPropertiesModel.php +++ b/src/Model/UserPropertiesModel.php @@ -24,10 +24,10 @@ class UserPropertiesModel /** * 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; From ff33e8f3a16f2262acd8daafbc988518c9010d20 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Sun, 16 Nov 2025 13:55:43 -0500 Subject: [PATCH 32/40] Refactor `UserPropertiesRepository`: integrate `UserProperty` enum for field mapping, update type handling, and enhance query processing with field mappers and update functions. --- src/Repository/UserPropertiesRepository.php | 66 ++++++++++++++++----- 1 file changed, 51 insertions(+), 15 deletions(-) diff --git a/src/Repository/UserPropertiesRepository.php b/src/Repository/UserPropertiesRepository.php index 01e1024..0931342 100644 --- a/src/Repository/UserPropertiesRepository.php +++ b/src/Repository/UserPropertiesRepository.php @@ -5,18 +5,21 @@ use ByJG\AnyDataset\Core\Exception\DatabaseException; use ByJG\AnyDataset\Db\DatabaseExecutor; use ByJG\AnyDataset\Db\Exception\DbDriverNotConnected; +use ByJG\Authenticate\Enum\UserProperty; use ByJG\Authenticate\Model\UserPropertiesModel; use ByJG\MicroOrm\DeleteQuery; use ByJG\MicroOrm\Exception\OrmBeforeInvalidException; use ByJG\MicroOrm\Exception\OrmInvalidFieldsException; use ByJG\MicroOrm\Exception\OrmModelInvalidException; use ByJG\MicroOrm\Exception\RepositoryReadOnlyException; +use ByJG\MicroOrm\Exception\UpdateConstraintException; use ByJG\MicroOrm\Literal\Literal; use ByJG\MicroOrm\Mapper; use ByJG\MicroOrm\Query; use ByJG\MicroOrm\Repository; use ByJG\XmlUtil\Exception\FileException; use ByJG\XmlUtil\Exception\XmlUtilException; +use Psr\SimpleCache\InvalidArgumentException; use ReflectionException; /** @@ -28,8 +31,11 @@ class UserPropertiesRepository protected Mapper $mapper; /** + * @param DatabaseExecutor $executor + * @param string $propertiesClass * @throws OrmModelInvalidException * @throws ReflectionException + * @throws \ByJG\MicroOrm\Exception\InvalidArgumentException */ public function __construct(DatabaseExecutor $executor, string $propertiesClass) { @@ -42,8 +48,16 @@ public function __construct(DatabaseExecutor $executor, string $propertiesClass) * * @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 { @@ -60,14 +74,19 @@ public function save(UserPropertiesModel $model): UserPropertiesModel * @throws DbDriverNotConnected * @throws FileException * @throws XmlUtilException - * @throws \Psr\SimpleCache\InvalidArgumentException + * @throws InvalidArgumentException */ public function getByUserId(string|Literal|int $userid): array { - $useridField = $this->mapper->getFieldMap('userid')->getFieldName(); + $userIdMapping = $this->mapper->getFieldMap(UserProperty::Userid->value); + $userIdUpdateFunction = $userIdMapping->getUpdateFunction(); + if (is_string($userIdUpdateFunction)) { + $userIdUpdateFunction = new $userIdUpdateFunction(); + } + $userIdField = $userIdMapping->getFieldName(); $query = Query::getInstance() ->table($this->mapper->getTable()) - ->where("$useridField = :userid", ['userid' => $userid]); + ->where("$userIdField = :userid", ['userid' => $userIdUpdateFunction->processedValue($userid, null)]); return $this->repository->getByQuery($query); } @@ -82,16 +101,23 @@ public function getByUserId(string|Literal|int $userid): array * @throws DbDriverNotConnected * @throws FileException * @throws XmlUtilException - * @throws \Psr\SimpleCache\InvalidArgumentException + * @throws InvalidArgumentException */ public function getByUserIdAndName(string|Literal|int $userid, string $propertyName): array { - $useridField = $this->mapper->getFieldMap('userid')->getFieldName(); - $nameField = $this->mapper->getFieldMap('name')->getFieldName(); + $userIdMapping = $this->mapper->getFieldMap(UserProperty::Userid->value); + $userIdUpdateFunction = $userIdMapping->getUpdateFunction(); + if (is_string($userIdUpdateFunction)) { + $userIdUpdateFunction = new $userIdUpdateFunction(); + } + $nameMapping = $this->mapper->getFieldMap(UserProperty::Name->value); + + $userIdField = $userIdMapping->getFieldName(); + $nameField = $nameMapping->getFieldName(); $query = Query::getInstance() ->table($this->mapper->getTable()) - ->where("$useridField = :userid", ['userid' => $userid]) + ->where("$userIdField = :userid", ['userid' => $userIdUpdateFunction->processedValue($userid, null)]) ->where("$nameField = :name", ['name' => $propertyName]); return $this->repository->getByQuery($query); @@ -105,14 +131,19 @@ public function getByUserIdAndName(string|Literal|int $userid, string $propertyN * @throws DatabaseException * @throws DbDriverNotConnected * @throws RepositoryReadOnlyException + * @throws \ByJG\MicroOrm\Exception\InvalidArgumentException */ public function deleteByUserId(string|Literal|int $userid): void { - $useridField = $this->mapper->getFieldMap('userid')->getFieldName(); + $userIdMapping = $this->mapper->getFieldMap(UserProperty::Userid->value); + $userIdUpdateFunction = $userIdMapping->getUpdateFunction(); + if (is_string($userIdUpdateFunction)) { + $userIdUpdateFunction = new $userIdUpdateFunction(); + } $userIdField = $userIdMapping->getFieldName(); $deleteQuery = DeleteQuery::getInstance() ->table($this->mapper->getTable()) - ->where("$useridField = :userid", ['userid' => $userid]); + ->where("$userIdField = :userid", ['userid' => $userIdUpdateFunction->processedValue($userid, null)]); $this->repository->deleteByQuery($deleteQuery); } @@ -130,13 +161,18 @@ public function deleteByUserId(string|Literal|int $userid): void */ public function deleteByUserIdAndName(string|Literal|int $userid, string $propertyName, ?string $value = null): void { - $useridField = $this->mapper->getFieldMap('userid')->getFieldName(); - $nameField = $this->mapper->getFieldMap('name')->getFieldName(); - $valueField = $this->mapper->getFieldMap('value')->getFieldName(); + $userIdMapping = $this->mapper->getFieldMap(UserProperty::Userid->value); + $userIdField = $userIdMapping->getFieldName(); + $userIdUpdateFunction = $userIdMapping->getUpdateFunction(); + if (is_string($userIdUpdateFunction)) { + $userIdUpdateFunction = new $userIdUpdateFunction(); + } + $nameField = $this->mapper->getFieldMap(UserProperty::Name->value)->getFieldName(); + $valueField = $this->mapper->getFieldMap(UserProperty::Value->value)->getFieldName(); $deleteQuery = DeleteQuery::getInstance() ->table($this->mapper->getTable()) - ->where("$useridField = :userid", ['userid' => $userid]) + ->where("$userIdField = :userid", ['userid' => $userIdUpdateFunction->processedValue($userid, null)]) ->where("$nameField = :name", ['name' => $propertyName]); if ($value !== null) { @@ -158,8 +194,8 @@ public function deleteByUserIdAndName(string|Literal|int $userid, string $proper */ public function deleteByName(string $propertyName, ?string $value = null): void { - $nameField = $this->mapper->getFieldMap('name')->getFieldName(); - $valueField = $this->mapper->getFieldMap('value')->getFieldName(); + $nameField = $this->mapper->getFieldMap(UserProperty::Name->value)->getFieldName(); + $valueField = $this->mapper->getFieldMap(UserProperty::Value->value)->getFieldName(); $deleteQuery = DeleteQuery::getInstance() ->table($this->mapper->getTable()) From 1c459b0d70296d0198208f0b03aec57ac8798aa6 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Sun, 16 Nov 2025 14:08:02 -0500 Subject: [PATCH 33/40] Refactor `UserPropertiesRepository`: integrate `UserProperty` enum for field mapping, update type handling, and enhance query processing with field mappers and update functions. --- src/Service/UsersService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service/UsersService.php b/src/Service/UsersService.php index 22b50ff..ced63e3 100644 --- a/src/Service/UsersService.php +++ b/src/Service/UsersService.php @@ -522,7 +522,7 @@ public function createAuthToken( return new UserToken( user: $user, token: $token, - data: $tokenUserFields + data: $updateTokenInfo ); } From c32bdf2a7ca303ec244c01ccbcb7ef4d68a57563 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Sun, 16 Nov 2025 14:23:29 -0500 Subject: [PATCH 34/40] Refactor `UserPropertiesRepository`: integrate `UserProperty` enum for field mapping, update type handling, and enhance query processing with field mappers and update functions. --- docs/authentication.md | 31 +++- docs/examples.md | 198 +++++++++++++++++++++++ docs/jwt-tokens.md | 56 +++++++ src/Interfaces/UsersServiceInterface.php | 9 ++ src/Service/UsersService.php | 54 +++++-- tests/TestUsersBase.php | 185 +++++++++++++++++++++ 6 files changed, 520 insertions(+), 13 deletions(-) diff --git a/docs/authentication.md b/docs/authentication.md index 4c7793f..352aa04 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -40,7 +40,7 @@ $users->save($user); ``` :::warning SHA-1 Deprecation -SHA-1 is used for backward compatibility. For new projects, consider implementing a custom password hasher using bcrypt or Argon2. See [Mappers](mappers.md#example-bcrypt-password-mapper) for details. +SHA-1 is used for backward compatibility. For new projects, consider implementing a custom password hasher using bcrypt or Argon2. See [Mappers](mappers.md) for details. ::: :::tip Enforce Password Strength @@ -77,6 +77,35 @@ if ($userToken !== null) { 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 diff --git a/docs/examples.md b/docs/examples.md index bf82345..4ea2496 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -544,6 +544,204 @@ print_r($permissions); $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 diff --git a/docs/jwt-tokens.md b/docs/jwt-tokens.md index 1499608..3104e61 100644 --- a/docs/jwt-tokens.md +++ b/docs/jwt-tokens.md @@ -125,6 +125,62 @@ $userToken = $users->createAuthToken( 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 diff --git a/src/Interfaces/UsersServiceInterface.php b/src/Interfaces/UsersServiceInterface.php index ca51a47..af39dd4 100644 --- a/src/Interfaces/UsersServiceInterface.php +++ b/src/Interfaces/UsersServiceInterface.php @@ -186,6 +186,15 @@ public function createAuthToken( array $tokenUserFields = [] ): ?UserToken; + public function createInsecureAuthToken( + UserModel|string $login, + JwtWrapper $jwtWrapper, + int $expires = 1200, + array $updateUserInfo = [], + array $updateTokenInfo = [], + array $tokenUserFields = [] + ): ?UserToken; + /** * Validate authentication token * diff --git a/src/Service/UsersService.php b/src/Service/UsersService.php index ced63e3..de1d156 100644 --- a/src/Service/UsersService.php +++ b/src/Service/UsersService.php @@ -477,21 +477,22 @@ public function getUsersByPropertySet(array $propertiesArray): array return $users; } - /** - * @inheritDoc - */ #[\Override] - public function createAuthToken( - string $login, - string $password, - JwtWrapper $jwtWrapper, - int $expires = 1200, - array $updateUserInfo = [], - array $updateTokenInfo = [], - array $tokenUserFields = [User::Userid, User::Name, User::Role] + public function createInsecureAuthToken( + string|UserModel $login, + JwtWrapper $jwtWrapper, + int $expires = 1200, + array $updateUserInfo = [], + array $updateTokenInfo = [], + array $tokenUserFields = [User::Userid, User::Name, User::Role] ): ?UserToken { - $user = $this->isValidUser($login, $password); + if (is_string($login)) { + $user = $this->getByLogin($login); + } else { + $user = $login; + } + if (is_null($user)) { throw new UserNotFoundException('User not found'); } @@ -526,6 +527,35 @@ public function createAuthToken( ); } + /** + * @inheritDoc + */ + #[\Override] + public function createAuthToken( + string $login, + string $password, + JwtWrapper $jwtWrapper, + int $expires = 1200, + array $updateUserInfo = [], + array $updateTokenInfo = [], + array $tokenUserFields = [User::Userid, User::Name, User::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 diff --git a/tests/TestUsersBase.php b/tests/TestUsersBase.php index cc0a25d..c4d5d45 100644 --- a/tests/TestUsersBase.php +++ b/tests/TestUsersBase.php @@ -5,6 +5,9 @@ use ByJG\Authenticate\Enum\LoginField; use ByJG\Authenticate\Exception\NotAuthenticatedException; use ByJG\Authenticate\Exception\UserExistsException; +use ByJG\Authenticate\Exception\UserNotFoundException; +use ByJG\Authenticate\Model\UserModel; +use ByJG\Authenticate\Model\UserToken; use ByJG\Authenticate\Service\UsersService; use ByJG\JwtWrapper\JwtHashHmacSecret; use ByJG\JwtWrapper\JwtWrapper; @@ -238,6 +241,188 @@ protected function expectedToken(string $tokenData, string $login, int $userId): */ 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 + ); + + $this->assertInstanceOf(UserToken::class, $userToken); + $this->assertNotEmpty($userToken->token); + $this->assertInstanceOf(UserModel::class, $userToken->user); + $this->assertIsArray($userToken->data); + $this->assertEquals(2, $userToken->user->getUserid()); + } + + 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->assertArrayHasKey('role', $userToken->data); + $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); From d0896b547d13b41b652c7c65b9814cd800b02a22 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Sun, 16 Nov 2025 14:26:04 -0500 Subject: [PATCH 35/40] Refactor `UserPropertiesRepository`: integrate `UserProperty` enum for field mapping, update type handling, and enhance query processing with field mappers and update functions. --- tests/TestUsersBase.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/TestUsersBase.php b/tests/TestUsersBase.php index c4d5d45..5f45c27 100644 --- a/tests/TestUsersBase.php +++ b/tests/TestUsersBase.php @@ -256,7 +256,6 @@ public function testCreateAuthTokenReturnsUserToken(): void $this->assertInstanceOf(UserToken::class, $userToken); $this->assertNotEmpty($userToken->token); $this->assertInstanceOf(UserModel::class, $userToken->user); - $this->assertIsArray($userToken->data); $this->assertEquals(2, $userToken->user->getUserid()); } From c7dabfed058901aa4dbb3bc70735091da361ecff Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Sun, 16 Nov 2025 14:44:52 -0500 Subject: [PATCH 36/40] Refactor `UserPropertiesRepository`: integrate `UserProperty` enum for field mapping, update type handling, and enhance query processing with field mappers and update functions. --- docs/jwt-tokens.md | 4 +- src/Enum/{User.php => UserField.php} | 2 +- ...UserProperty.php => UserPropertyField.php} | 2 +- src/Repository/UserPropertiesRepository.php | 20 +++---- src/Service/UsersService.php | 60 +++++++++---------- 5 files changed, 44 insertions(+), 44 deletions(-) rename src/Enum/{User.php => UserField.php} (93%) rename src/Enum/{UserProperty.php => UserPropertyField.php} (85%) diff --git a/docs/jwt-tokens.md b/docs/jwt-tokens.md index 3104e61..22cd7f5 100644 --- a/docs/jwt-tokens.md +++ b/docs/jwt-tokens.md @@ -110,7 +110,7 @@ Instead of manually adding every field to `$updateTokenInfo`, pass a seventh arg ```php createAuthToken( 'johndoe', @@ -119,7 +119,7 @@ $userToken = $users->createAuthToken( 3600, [], [], - [User::Name, User::Email, 'department'] + [UserField::Name, UserField::Email, 'department'] ); ``` diff --git a/src/Enum/User.php b/src/Enum/UserField.php similarity index 93% rename from src/Enum/User.php rename to src/Enum/UserField.php index ca83773..efc5e79 100644 --- a/src/Enum/User.php +++ b/src/Enum/UserField.php @@ -2,7 +2,7 @@ namespace ByJG\Authenticate\Enum; -enum User: string +enum UserField: string { // User field name constants case Userid = 'userid'; diff --git a/src/Enum/UserProperty.php b/src/Enum/UserPropertyField.php similarity index 85% rename from src/Enum/UserProperty.php rename to src/Enum/UserPropertyField.php index 4a03707..69b9a9e 100644 --- a/src/Enum/UserProperty.php +++ b/src/Enum/UserPropertyField.php @@ -2,7 +2,7 @@ namespace ByJG\Authenticate\Enum; -enum UserProperty: string +enum UserPropertyField: string { // User properties field name constants case Id = 'id'; diff --git a/src/Repository/UserPropertiesRepository.php b/src/Repository/UserPropertiesRepository.php index 0931342..322701e 100644 --- a/src/Repository/UserPropertiesRepository.php +++ b/src/Repository/UserPropertiesRepository.php @@ -5,7 +5,7 @@ use ByJG\AnyDataset\Core\Exception\DatabaseException; use ByJG\AnyDataset\Db\DatabaseExecutor; use ByJG\AnyDataset\Db\Exception\DbDriverNotConnected; -use ByJG\Authenticate\Enum\UserProperty; +use ByJG\Authenticate\Enum\UserPropertyField; use ByJG\Authenticate\Model\UserPropertiesModel; use ByJG\MicroOrm\DeleteQuery; use ByJG\MicroOrm\Exception\OrmBeforeInvalidException; @@ -78,7 +78,7 @@ public function save(UserPropertiesModel $model): UserPropertiesModel */ public function getByUserId(string|Literal|int $userid): array { - $userIdMapping = $this->mapper->getFieldMap(UserProperty::Userid->value); + $userIdMapping = $this->mapper->getFieldMap(UserPropertyField::Userid->value); $userIdUpdateFunction = $userIdMapping->getUpdateFunction(); if (is_string($userIdUpdateFunction)) { $userIdUpdateFunction = new $userIdUpdateFunction(); @@ -105,12 +105,12 @@ public function getByUserId(string|Literal|int $userid): array */ public function getByUserIdAndName(string|Literal|int $userid, string $propertyName): array { - $userIdMapping = $this->mapper->getFieldMap(UserProperty::Userid->value); + $userIdMapping = $this->mapper->getFieldMap(UserPropertyField::Userid->value); $userIdUpdateFunction = $userIdMapping->getUpdateFunction(); if (is_string($userIdUpdateFunction)) { $userIdUpdateFunction = new $userIdUpdateFunction(); } - $nameMapping = $this->mapper->getFieldMap(UserProperty::Name->value); + $nameMapping = $this->mapper->getFieldMap(UserPropertyField::Name->value); $userIdField = $userIdMapping->getFieldName(); $nameField = $nameMapping->getFieldName(); @@ -135,7 +135,7 @@ public function getByUserIdAndName(string|Literal|int $userid, string $propertyN */ public function deleteByUserId(string|Literal|int $userid): void { - $userIdMapping = $this->mapper->getFieldMap(UserProperty::Userid->value); + $userIdMapping = $this->mapper->getFieldMap(UserPropertyField::Userid->value); $userIdUpdateFunction = $userIdMapping->getUpdateFunction(); if (is_string($userIdUpdateFunction)) { $userIdUpdateFunction = new $userIdUpdateFunction(); @@ -161,14 +161,14 @@ public function deleteByUserId(string|Literal|int $userid): void */ public function deleteByUserIdAndName(string|Literal|int $userid, string $propertyName, ?string $value = null): void { - $userIdMapping = $this->mapper->getFieldMap(UserProperty::Userid->value); + $userIdMapping = $this->mapper->getFieldMap(UserPropertyField::Userid->value); $userIdField = $userIdMapping->getFieldName(); $userIdUpdateFunction = $userIdMapping->getUpdateFunction(); if (is_string($userIdUpdateFunction)) { $userIdUpdateFunction = new $userIdUpdateFunction(); } - $nameField = $this->mapper->getFieldMap(UserProperty::Name->value)->getFieldName(); - $valueField = $this->mapper->getFieldMap(UserProperty::Value->value)->getFieldName(); + $nameField = $this->mapper->getFieldMap(UserPropertyField::Name->value)->getFieldName(); + $valueField = $this->mapper->getFieldMap(UserPropertyField::Value->value)->getFieldName(); $deleteQuery = DeleteQuery::getInstance() ->table($this->mapper->getTable()) @@ -194,8 +194,8 @@ public function deleteByUserIdAndName(string|Literal|int $userid, string $proper */ public function deleteByName(string $propertyName, ?string $value = null): void { - $nameField = $this->mapper->getFieldMap(UserProperty::Name->value)->getFieldName(); - $valueField = $this->mapper->getFieldMap(UserProperty::Value->value)->getFieldName(); + $nameField = $this->mapper->getFieldMap(UserPropertyField::Name->value)->getFieldName(); + $valueField = $this->mapper->getFieldMap(UserPropertyField::Value->value)->getFieldName(); $deleteQuery = DeleteQuery::getInstance() ->table($this->mapper->getTable()) diff --git a/src/Service/UsersService.php b/src/Service/UsersService.php index de1d156..51b37f2 100644 --- a/src/Service/UsersService.php +++ b/src/Service/UsersService.php @@ -4,8 +4,8 @@ use ByJG\Authenticate\Definition\PasswordDefinition; use ByJG\Authenticate\Enum\LoginField; -use ByJG\Authenticate\Enum\User; -use ByJG\Authenticate\Enum\UserProperty; +use ByJG\Authenticate\Enum\UserField; +use ByJG\Authenticate\Enum\UserPropertyField; use ByJG\Authenticate\Exception\NotAuthenticatedException; use ByJG\Authenticate\Exception\UserExistsException; use ByJG\Authenticate\Exception\UserNotFoundException; @@ -46,19 +46,19 @@ public function __construct( $this->loginField = $loginField; $userMapper = $usersRepository->getRepository()->getMapper(); - $userCheck = ($userMapper->getFieldMap(User::Userid->value) !== null) && - ($userMapper->getFieldMap(User::Name->value) !== null) && - ($userMapper->getFieldMap(User::Email->value) !== null) && - ($userMapper->getFieldMap(User::Username->value) !== null) && - ($userMapper->getFieldMap(User::Password->value) !== null) && - ($userMapper->getFieldMap(User::Role->value) !== null); + $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(User::Password->value)->getUpdateFunction(); + $passwordUpdateFunction = $userMapper->getFieldMap(UserField::Password->value)->getUpdateFunction(); if (!is_subclass_of($passwordUpdateFunction, PasswordMapperInterface::class, true)) { throw new InvalidArgumentException('Password update function must implement PasswordMapperInterface'); } @@ -66,9 +66,9 @@ public function __construct( $this->passwordDefinition?->setPasswordMapper($passwordUpdateFunction); $propertyMapper = $propertiesRepository->getRepository()->getMapper(); - $propertyCheck = ($propertyMapper->getFieldMap(UserProperty::Userid->value) !== null) && - ($propertyMapper->getFieldMap(UserProperty::Name->value) !== null) && - ($propertyMapper->getFieldMap(UserProperty::Value->value) !== null); + $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'); @@ -135,9 +135,9 @@ public function addUser(string $name, string $userName, string $email, string $p $mapper = $this->usersRepository->getMapper(); $model = $mapper->getEntity([ - $mapper->getFieldMap(User::Name->value)->getFieldName() => $name, - $mapper->getFieldMap(User::Email->value)->getFieldName() => $email, - $mapper->getFieldMap(User::Username->value)->getFieldName() => $userName, + $mapper->getFieldMap(UserField::Name->value)->getFieldName() => $name, + $mapper->getFieldMap(UserField::Email->value)->getFieldName() => $email, + $mapper->getFieldMap(UserField::Username->value)->getFieldName() => $userName, ]); $this->applyPasswordDefinition($model); $model->setPassword($password); @@ -185,7 +185,7 @@ public function getById(string|Literal|int $userid): ?UserModel #[\Override] public function getByEmail(string $email): ?UserModel { - $fieldMap = $this->usersRepository->getMapper()->getFieldMap(User::Email->value); + $fieldMap = $this->usersRepository->getMapper()->getFieldMap(UserField::Email->value); $user = $this->usersRepository->getByField($fieldMap->getFieldName(), $email); if ($user !== null) { $this->loadUserProperties($user); @@ -200,7 +200,7 @@ public function getByEmail(string $email): ?UserModel #[\Override] public function getByUsername(string $username): ?UserModel { - $fieldMap = $this->usersRepository->getMapper()->getFieldMap(User::Username->value); + $fieldMap = $this->usersRepository->getMapper()->getFieldMap(UserField::Username->value); $user = $this->usersRepository->getByField($fieldMap->getFieldName(), $username); if ($user !== null) { $this->loadUserProperties($user); @@ -298,7 +298,7 @@ public function isValidUser(string $login, string $password): ?UserModel } // Hash the password for comparison using the model's configured password mapper - $passwordFieldMapping = $this->usersRepository->getMapper()->getFieldMap(User::Password->value); + $passwordFieldMapping = $this->usersRepository->getMapper()->getFieldMap(UserField::Password->value); $hashedPassword = $passwordFieldMapping->getUpdateFunctionValue($password, null); if ($user->getPassword() === $hashedPassword) { @@ -371,9 +371,9 @@ public function addProperty(string|Literal|int $userId, string $propertyName, ?s if (!$this->hasProperty($userId, $propertyName, $value)) { $propMapper = $this->propertiesRepository->getMapper(); $property = $propMapper->getEntity([ - $propMapper->getFieldMap(UserProperty::Userid->value)->getFieldName() => $userId, - $propMapper->getFieldMap(UserProperty::Name->value)->getFieldName() => $propertyName, - $propMapper->getFieldMap(UserProperty::Value->value)->getFieldName() => $value + $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); } @@ -392,9 +392,9 @@ public function setProperty(string|Literal|int $userId, string $propertyName, ?s if (empty($properties)) { $propMapper = $this->propertiesRepository->getMapper(); $property = $propMapper->getEntity([ - $propMapper->getFieldMap(UserProperty::Userid->value)->getFieldName() => $userId, - $propMapper->getFieldMap(UserProperty::Name->value)->getFieldName() => $propertyName, - $propMapper->getFieldMap(UserProperty::Value->value)->getFieldName() => $value + $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]; @@ -452,9 +452,9 @@ public function getUsersByPropertySet(array $propertiesArray): array $userPk = $userPk[0]; } - $propUserIdField = $this->propertiesRepository->getMapper()->getFieldMap(UserProperty::Userid->value)->getFieldName(); - $propNameField = $this->propertiesRepository->getMapper()->getFieldMap(UserProperty::Name->value)->getFieldName(); - $propValueField = $this->propertiesRepository->getMapper()->getFieldMap(UserProperty::Value->value)->getFieldName(); + $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.*") @@ -484,7 +484,7 @@ public function createInsecureAuthToken( int $expires = 1200, array $updateUserInfo = [], array $updateTokenInfo = [], - array $tokenUserFields = [User::Userid, User::Name, User::Role] + array $tokenUserFields = [UserField::Userid, UserField::Name, UserField::Role] ): ?UserToken { if (is_string($login)) { @@ -502,7 +502,7 @@ public function createInsecureAuthToken( } foreach ($tokenUserFields as $field) { - $fieldName = $field instanceof User ? $field->value : $field; + $fieldName = $field instanceof UserField ? $field->value : $field; if (!is_string($fieldName) || $fieldName === '') { continue; } @@ -538,7 +538,7 @@ public function createAuthToken( int $expires = 1200, array $updateUserInfo = [], array $updateTokenInfo = [], - array $tokenUserFields = [User::Userid, User::Name, User::Role] + array $tokenUserFields = [UserField::Userid, UserField::Name, UserField::Role] ): ?UserToken { $user = $this->isValidUser($login, $password); From 7707b59f0f936ddf838d775ef284159fd798eb4a Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Sun, 16 Nov 2025 15:17:42 -0500 Subject: [PATCH 37/40] Refactor `UserPropertiesRepository`: integrate `UserProperty` enum for field mapping, update type handling, and enhance query processing with field mappers and update functions. --- src/Repository/UserPropertiesRepository.php | 25 +++++---------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/src/Repository/UserPropertiesRepository.php b/src/Repository/UserPropertiesRepository.php index 322701e..874bc9b 100644 --- a/src/Repository/UserPropertiesRepository.php +++ b/src/Repository/UserPropertiesRepository.php @@ -79,14 +79,10 @@ public function save(UserPropertiesModel $model): UserPropertiesModel public function getByUserId(string|Literal|int $userid): array { $userIdMapping = $this->mapper->getFieldMap(UserPropertyField::Userid->value); - $userIdUpdateFunction = $userIdMapping->getUpdateFunction(); - if (is_string($userIdUpdateFunction)) { - $userIdUpdateFunction = new $userIdUpdateFunction(); - } $userIdField = $userIdMapping->getFieldName(); $query = Query::getInstance() ->table($this->mapper->getTable()) - ->where("$userIdField = :userid", ['userid' => $userIdUpdateFunction->processedValue($userid, null)]); + ->where("$userIdField = :userid", ['userid' => $userIdMapping->getUpdateFunctionValue($userid, null)]); return $this->repository->getByQuery($query); } @@ -106,10 +102,6 @@ public function getByUserId(string|Literal|int $userid): array public function getByUserIdAndName(string|Literal|int $userid, string $propertyName): array { $userIdMapping = $this->mapper->getFieldMap(UserPropertyField::Userid->value); - $userIdUpdateFunction = $userIdMapping->getUpdateFunction(); - if (is_string($userIdUpdateFunction)) { - $userIdUpdateFunction = new $userIdUpdateFunction(); - } $nameMapping = $this->mapper->getFieldMap(UserPropertyField::Name->value); $userIdField = $userIdMapping->getFieldName(); @@ -117,7 +109,7 @@ public function getByUserIdAndName(string|Literal|int $userid, string $propertyN $query = Query::getInstance() ->table($this->mapper->getTable()) - ->where("$userIdField = :userid", ['userid' => $userIdUpdateFunction->processedValue($userid, null)]) + ->where("$userIdField = :userid", ['userid' => $userIdMapping->getUpdateFunctionValue($userid, null)]) ->where("$nameField = :name", ['name' => $propertyName]); return $this->repository->getByQuery($query); @@ -136,14 +128,11 @@ public function getByUserIdAndName(string|Literal|int $userid, string $propertyN public function deleteByUserId(string|Literal|int $userid): void { $userIdMapping = $this->mapper->getFieldMap(UserPropertyField::Userid->value); - $userIdUpdateFunction = $userIdMapping->getUpdateFunction(); - if (is_string($userIdUpdateFunction)) { - $userIdUpdateFunction = new $userIdUpdateFunction(); - } $userIdField = $userIdMapping->getFieldName(); + $userIdField = $userIdMapping->getFieldName(); $deleteQuery = DeleteQuery::getInstance() ->table($this->mapper->getTable()) - ->where("$userIdField = :userid", ['userid' => $userIdUpdateFunction->processedValue($userid, null)]); + ->where("$userIdField = :userid", ['userid' => $userIdMapping->getUpdateFunctionValue($userid, null)]); $this->repository->deleteByQuery($deleteQuery); } @@ -163,16 +152,12 @@ public function deleteByUserIdAndName(string|Literal|int $userid, string $proper { $userIdMapping = $this->mapper->getFieldMap(UserPropertyField::Userid->value); $userIdField = $userIdMapping->getFieldName(); - $userIdUpdateFunction = $userIdMapping->getUpdateFunction(); - if (is_string($userIdUpdateFunction)) { - $userIdUpdateFunction = new $userIdUpdateFunction(); - } $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' => $userIdUpdateFunction->processedValue($userid, null)]) + ->where("$userIdField = :userid", ['userid' => $userIdMapping->getUpdateFunctionValue($userid, null)]) ->where("$nameField = :name", ['name' => $propertyName]); if ($value !== null) { From 1855fdd00a7f9642a91f9fd7e6b0ccc134531f76 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Mon, 17 Nov 2025 14:15:23 -0500 Subject: [PATCH 38/40] Refactor `UsersService`: enhance `resolveUserFieldValue` to support default values and improve token user field handling logic. --- src/Service/UsersService.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Service/UsersService.php b/src/Service/UsersService.php index 51b37f2..6b548a8 100644 --- a/src/Service/UsersService.php +++ b/src/Service/UsersService.php @@ -239,7 +239,7 @@ protected function applyPasswordDefinition(UserModel $user): void } } - protected function resolveUserFieldValue(UserModel $user, string $fieldName): mixed + protected function resolveUserFieldValue(UserModel $user, string $fieldName, ?string $default = null): mixed { $camel = str_replace(' ', '', ucwords(str_replace(['_', '-'], ' ' , $fieldName))); $method = 'get' . $camel; @@ -252,7 +252,7 @@ protected function resolveUserFieldValue(UserModel $user, string $fieldName): mi return $value; } - return null; + return $default; } /** @@ -501,13 +501,18 @@ public function createInsecureAuthToken( $user->set($key, $value); } - foreach ($tokenUserFields as $field) { + 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); + $value = $this->resolveUserFieldValue($user, $fieldName, $default); if ($value !== null) { $updateTokenInfo[$fieldName] = $value; } From cb2d2533a4850bbf62099240c087b47a7f58462a Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Mon, 17 Nov 2025 14:30:22 -0500 Subject: [PATCH 39/40] Refactor `UsersService` and tests: update `addUser` to support optional `role` parameter, enhance `resolveUserFieldValue` with improved default handling, and adjust workflows for compatibility. --- .github/workflows/phpunit.yml | 6 ++++-- src/Interfaces/UsersServiceInterface.php | 3 ++- src/Service/UsersService.php | 15 +++++++-------- tests/TestUsersBase.php | 11 ++++++++++- 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 300e54f..7d2e33a 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -12,7 +12,9 @@ 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: @@ -21,7 +23,7 @@ jobs: - "8.2" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - run: composer install - run: ./vendor/bin/psalm - run: ./vendor/bin/phpunit --stderr diff --git a/src/Interfaces/UsersServiceInterface.php b/src/Interfaces/UsersServiceInterface.php index af39dd4..6ee739f 100644 --- a/src/Interfaces/UsersServiceInterface.php +++ b/src/Interfaces/UsersServiceInterface.php @@ -28,9 +28,10 @@ public function save(UserModel $model): UserModel; * @param string $userName * @param string $email * @param string $password + * @param string|null $role * @return UserModel */ - public function addUser(string $name, string $userName, string $email, string $password): UserModel; + public function addUser(string $name, string $userName, string $email, string $password, ?string $role = null): UserModel; /** * Get user by ID diff --git a/src/Service/UsersService.php b/src/Service/UsersService.php index 6b548a8..53cc21f 100644 --- a/src/Service/UsersService.php +++ b/src/Service/UsersService.php @@ -127,10 +127,11 @@ public function save(UserModel $model): UserModel } /** + * @param string|null $role * @inheritDoc */ #[\Override] - public function addUser(string $name, string $userName, string $email, string $password): UserModel + public function addUser(string $name, string $userName, string $email, string $password, ?string $role = null): UserModel { $mapper = $this->usersRepository->getMapper(); @@ -138,6 +139,7 @@ public function addUser(string $name, string $userName, string $email, string $p $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); @@ -244,15 +246,12 @@ protected function resolveUserFieldValue(UserModel $user, string $fieldName, ?st $camel = str_replace(' ', '', ucwords(str_replace(['_', '-'], ' ' , $fieldName))); $method = 'get' . $camel; if (method_exists($user, $method)) { - return $user->$method(); - } - - $value = $user->get($fieldName); - if ($value !== null) { - return $value; + $value = $user->$method(); + } else { + $value = $user->get($fieldName); } - return $default; + return empty($value) ? $default : $value; } /** diff --git a/tests/TestUsersBase.php b/tests/TestUsersBase.php index 5f45c27..2791abd 100644 --- a/tests/TestUsersBase.php +++ b/tests/TestUsersBase.php @@ -3,6 +3,7 @@ namespace Tests; use ByJG\Authenticate\Enum\LoginField; +use ByJG\Authenticate\Enum\UserField; use ByJG\Authenticate\Exception\NotAuthenticatedException; use ByJG\Authenticate\Exception\UserExistsException; use ByJG\Authenticate\Exception\UserNotFoundException; @@ -250,13 +251,21 @@ public function testCreateAuthTokenReturnsUserToken(): void $login, 'pwd2', $jwtWrapper, - 1200 + 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 From 1c71d02e3b26665d441a4c5ac317db2ea0ec96fc Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Mon, 17 Nov 2025 14:53:51 -0500 Subject: [PATCH 40/40] Refactor `UsersService` and tests: update `addUser` to support optional `role` parameter, enhance `resolveUserFieldValue` with improved default handling, and adjust workflows for compatibility. --- tests/TestUsersBase.php | 12 ++++-------- tests/UsersDBDataset2ByUserNameTestUsersBase.php | 4 ++-- tests/UsersDBDatasetByUsernameTestUsersBase.php | 2 +- tests/UsersDBDatasetDefinitionTest.php | 2 +- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/tests/TestUsersBase.php b/tests/TestUsersBase.php index 2791abd..e58447d 100644 --- a/tests/TestUsersBase.php +++ b/tests/TestUsersBase.php @@ -174,7 +174,7 @@ public function testIsAdmin(): void // Check user has no role initially $user3 = $this->object->getById($this->prefix . '3'); $this->assertFalse($user3->hasRole('admin')); - $this->assertEmpty($user3->getRole()); + $this->assertEquals("foobar", $user3->getRole()); // Set a role $login = $this->__chooseValue('user3', 'user3@gmail.com'); @@ -220,7 +220,6 @@ protected function expectedToken(string $tokenData, string $login, int $userId): 'tokenData' => $tokenData, 'userid' => $userId, 'name' => $user->getName(), - 'role' => $user->getRole(), ]; $tokenResult = $this->object->isValidToken($loginCreated, $jwtWrapper, $userToken->token); @@ -230,11 +229,8 @@ protected function expectedToken(string $tokenData, string $login, int $userId): // Compare user fields (excluding timestamps which may differ) $this->assertEquals($user->getUserid(), $tokenResult->user->getUserid()); - $this->assertEquals($user->getName(), $tokenResult->user->getName()); - $this->assertEquals($user->getEmail(), $tokenResult->user->getEmail()); - $this->assertEquals($user->getUsername(), $tokenResult->user->getUsername()); - $this->assertEquals($user->getPassword(), $tokenResult->user->getPassword()); - $this->assertEquals($user->getRole(), $tokenResult->user->getRole()); + $this->assertEquals($user->get("userData"), "userValue"); + $this->assertEquals($user->get("userData"), $tokenResult->user->get("userData")); } /** @@ -332,7 +328,7 @@ public function testCreateAuthTokenIncludesDefaultUserFields(): void $this->assertArrayHasKey('userid', $userToken->data); $this->assertArrayHasKey('name', $userToken->data); - $this->assertArrayHasKey('role', $userToken->data); + $this->assertArrayNotHasKey('role', $userToken->data); // Role isn't set because is null $this->assertEquals(2, $userToken->data['userid']); } diff --git a/tests/UsersDBDataset2ByUserNameTestUsersBase.php b/tests/UsersDBDataset2ByUserNameTestUsersBase.php index 8d2469f..25c9687 100644 --- a/tests/UsersDBDataset2ByUserNameTestUsersBase.php +++ b/tests/UsersDBDataset2ByUserNameTestUsersBase.php @@ -51,9 +51,9 @@ public function __setUp($loginField) $loginField ); - $this->object->addUser('User 1', 'user1', 'user1@gmail.com', 'pwd1'); + $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'); + $this->object->addUser('User 3', 'user3', 'user3@gmail.com', 'pwd3', "foobar"); } #[\Override] diff --git a/tests/UsersDBDatasetByUsernameTestUsersBase.php b/tests/UsersDBDatasetByUsernameTestUsersBase.php index 2a71edc..4a5b266 100644 --- a/tests/UsersDBDatasetByUsernameTestUsersBase.php +++ b/tests/UsersDBDatasetByUsernameTestUsersBase.php @@ -64,7 +64,7 @@ public function __setUp($loginField) $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'); + $this->object->addUser('User 3', 'user3', 'user3@gmail.com', 'pwd3', 'foobar'); } #[\Override] diff --git a/tests/UsersDBDatasetDefinitionTest.php b/tests/UsersDBDatasetDefinitionTest.php index e1d9d8b..e3479d5 100644 --- a/tests/UsersDBDatasetDefinitionTest.php +++ b/tests/UsersDBDatasetDefinitionTest.php @@ -74,7 +74,7 @@ public function __setUp($loginField) new MyUserModel('User 2', 'user2@gmail.com', 'user2', 'pwd2', '', 'other 2') ); $this->object->save( - new MyUserModel('User 3', 'user3@gmail.com', 'user3', 'pwd3', '', 'other 3') + new MyUserModel('User 3', 'user3@gmail.com', 'user3', 'pwd3', 'foobar', 'other 3') ); }