From 10084cbe090ddec08837348e1cb350eaea93c2a0 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Sat, 19 Oct 2024 21:28:43 +0000 Subject: [PATCH 01/32] feat: update config --- .github/FUNDING.yml | 4 - .github/workflows/tests.yml | 43 ---------- alchemy.config.php | 26 ------ src/Auth.php | 158 ++++++++++++++++-------------------- src/Auth/Core.php | 56 +++++++------ src/functions.php | 32 -------- tests/AuthTest.php | 2 +- tests/Pest.php | 30 +++---- 8 files changed, 116 insertions(+), 235 deletions(-) delete mode 100644 .github/FUNDING.yml delete mode 100644 .github/workflows/tests.yml delete mode 100644 alchemy.config.php diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 16bf7a0..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,4 +0,0 @@ -# These are supported funding model platforms - -open_collective: leaf -github: leafsphp diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index 6ee6474..0000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Run Tests - -on: ['push', 'pull_request'] - -env: - MYSQL_DATABASE: leaf - DB_USER: root - DB_PASSWORD: root - -jobs: - ci: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest] - php: ['7.4', '8.0', '8.1', '8.2', '8.3'] - - name: PHP ${{ matrix.php }} - ${{ matrix.os }} - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Initialize MySQL - run: sudo systemctl start mysql.service - - - name: Initialize first database - run: | - mysql -e 'CREATE DATABASE ${{ env.MYSQL_DATABASE }};' \ - -u${{ env.DB_USER }} -p${{ env.DB_PASSWORD }} - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - tools: composer:v2 - coverage: xdebug - - - name: Install PHP dependencies - run: composer update --no-interaction --no-progress - - - name: All Tests - run: composer run-script test diff --git a/alchemy.config.php b/alchemy.config.php deleted file mode 100644 index 7224127..0000000 --- a/alchemy.config.php +++ /dev/null @@ -1,26 +0,0 @@ - 'pest', - - // php unit options - 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', - 'xsi:noNamespaceSchemaLocation' => './vendor/phpunit/phpunit/phpunit.xsd', - 'bootstrap' => 'vendor/autoload.php', - 'colors' => true, - - // you can have multiple testsuites - 'testsuites' => [ - 'directory' => './tests' - ], - - // coverage options - 'coverage' => [ - 'processUncoveredFiles' => true, - 'include' => [ - './app' => '.php', - './src' => '.php' - ] - ] -]; diff --git a/src/Auth.php b/src/Auth.php index b28d247..386100e 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -28,62 +28,56 @@ public static function login(array $credentials) { static::leafDbConnect(); - $table = static::$settings['DB_TABLE']; + $table = static::$settings['db.table']; - if (static::config('USE_SESSION')) { + if (static::config('session')) { static::useSession(); } - $passKey = static::$settings['PASSWORD_KEY']; + $passKey = static::$settings['password.key']; $password = $credentials[$passKey] ?? null; if (isset($credentials[$passKey])) { unset($credentials[$passKey]); } else { - static::$settings['AUTH_NO_PASS'] = true; + static::$settings['password'] = false; } $user = static::$db->select($table)->where($credentials)->fetchAssoc(); if (!$user) { - static::$errors['auth'] = static::$settings['LOGIN_PARAMS_ERROR']; + static::$errors['auth'] = static::$settings['messages.loginParamsError']; return false; } - if (static::$settings['AUTH_NO_PASS'] === false) { - $passwordIsValid = false; - - if (static::$settings['PASSWORD_VERIFY'] !== false && isset($user[$passKey])) { - if (is_callable(static::$settings['PASSWORD_VERIFY'])) { - $passwordIsValid = call_user_func(static::$settings['PASSWORD_VERIFY'], $password, $user[$passKey]); - } else if (static::$settings['PASSWORD_VERIFY'] === Password::MD5) { - $passwordIsValid = md5($password) === $user[$passKey]; - } else { - $passwordIsValid = Password::verify($password, $user[$passKey]); - } - } + if (static::$settings['password']) { + $passwordIsValid = (static::$settings['password.verify'] !== false && isset($user[$passKey])) + ? ((is_callable(static::$settings['password.verify'])) + ? call_user_func(static::$settings['password.verify'], $password, $user[$passKey]) + : Password::verify($password, $user[$passKey])) + : false; if (!$passwordIsValid) { - static::$errors['password'] = static::$settings['LOGIN_PASSWORD_ERROR']; + static::$errors['password'] = static::$settings['messages.loginPasswordError']; return false; } } $token = Authentication::generateSimpleToken( - $user[static::$settings['ID_KEY']], - static::config('TOKEN_SECRET'), - static::config('TOKEN_LIFETIME') + $user[static::$settings['id.key']], + static::config('token.secret'), + static::config('token.lifetime') ); - if (isset($user[static::$settings['ID_KEY']])) { - $userId = $user[static::$settings['ID_KEY']]; + if (isset($user[static::$settings['id.key']])) { + $userId = $user[static::$settings['id.key']]; - if (static::$settings['HIDE_ID']) { - unset($user[static::$settings['ID_KEY']]); + if (in_array(static::$settings['id.key'], static::$settings['hidden']) || in_array('field.id', static::$settings['hidden'])) { + unset($user[static::$settings['id.key']]); } } - if (static::$settings['HIDE_PASSWORD'] && (isset($user[$passKey]) || !$user[$passKey])) { + if ((in_array(static::$settings['password.key'], static::$settings['hidden']) || in_array('field.password', static::$settings['hidden'])) && (isset($user[$passKey]) || !$user[$passKey])) { unset($user[$passKey]); } @@ -92,16 +86,12 @@ public static function login(array $credentials) return false; } - if (static::config('USE_SESSION')) { + if (static::config('session')) { if (isset($userId)) { - $user[static::$settings['ID_KEY']] = $userId; + $user[static::$settings['id.key']] = $userId; } self::setUserToSession($user, $token); - - if (static::config('SESSION_REDIRECT_ON_LOGIN')) { - exit(header('location: ' . static::config('GUARD_HOME'))); - } } $response['user'] = $user; @@ -122,33 +112,27 @@ public static function register(array $credentials, array $uniques = []) { static::leafDbConnect(); - $table = static::$settings['DB_TABLE']; - $passKey = static::$settings['PASSWORD_KEY']; + $table = static::$settings['db.table']; + $passKey = static::$settings['password.key']; if (!isset($credentials[$passKey])) { - static::$settings['AUTH_NO_PASS'] = true; + static::$settings['password'] = false; } - if (static::$settings['AUTH_NO_PASS'] === false) { - if (static::$settings['PASSWORD_ENCODE'] !== false) { - if (is_callable(static::$settings['PASSWORD_ENCODE'])) { - $credentials[$passKey] = call_user_func(static::$settings['PASSWORD_ENCODE'], $credentials[$passKey]); - } else if (static::$settings['PASSWORD_ENCODE'] === 'md5') { - $credentials[$passKey] = md5($credentials[$passKey]); - } else { - $credentials[$passKey] = Password::hash($credentials[$passKey]); - } - } + if (static::$settings['password'] && static::$settings['password.encode'] !== false) { + $credentials[$passKey] = (is_callable(static::$settings['password.encode'])) + ? call_user_func(static::$settings['password.encode'], $credentials[$passKey]) + : Password::hash($credentials[$passKey]); } - if (static::$settings['USE_TIMESTAMPS']) { - $now = (new \Leaf\Date())->tick()->format(static::$settings['TIMESTAMP_FORMAT']); + if (static::$settings['timestamps']) { + $now = (new \Leaf\Date())->tick()->format(static::$settings['timestamps.format']); $credentials['created_at'] = $now; $credentials['updated_at'] = $now; } - if (static::$settings['USE_UUID'] !== false) { - $credentials[static::$settings['ID_KEY']] = static::$settings['USE_UUID']; + if (static::$settings['id.uuid'] !== false) { + $credentials[static::$settings['id.key']] = call_user_func(static::$settings['id.uuid']); } try { @@ -170,17 +154,17 @@ public static function register(array $credentials, array $uniques = []) } $token = Authentication::generateSimpleToken( - $user[static::$settings['ID_KEY']], - static::config('TOKEN_SECRET'), - static::config('TOKEN_LIFETIME') + $user[static::$settings['id.key']], + static::config('token.secret'), + static::config('token.lifetime') ); - if (isset($user[static::$settings['ID_KEY']])) { - $userId = $user[static::$settings['ID_KEY']]; + if (isset($user[static::$settings['id.key']])) { + $userId = $user[static::$settings['id.key']]; } if (static::$settings['HIDE_ID']) { - unset($user[static::$settings['ID_KEY']]); + unset($user[static::$settings['id.key']]); } if (static::$settings['HIDE_PASSWORD'] && (isset($user[$passKey]) || !$user[$passKey])) { @@ -192,10 +176,10 @@ public static function register(array $credentials, array $uniques = []) return false; } - if (static::config('USE_SESSION')) { + if (static::config('session')) { if (static::config('SESSION_ON_REGISTER')) { if (isset($userId)) { - $user[static::$settings['ID_KEY']] = $userId; + $user[static::$settings['id.key']] = $userId; } self::setUserToSession($user, $token); @@ -226,13 +210,13 @@ public static function update(array $credentials, array $uniques = []) { static::leafDbConnect(); - $table = static::$settings['DB_TABLE']; + $table = static::$settings['db.table']; - if (static::config('USE_SESSION')) { + if (static::config('session')) { static::useSession(); } - $passKey = static::$settings['PASSWORD_KEY']; + $passKey = static::$settings['password.key']; $loggedInUser = static::user(); if (!$loggedInUser) { @@ -240,27 +224,27 @@ public static function update(array $credentials, array $uniques = []) return false; } - $where = isset($loggedInUser[static::$settings['ID_KEY']]) ? [static::$settings['ID_KEY'] => $loggedInUser[static::$settings['ID_KEY']]] : $loggedInUser; + $where = isset($loggedInUser[static::$settings['id.key']]) ? [static::$settings['id.key'] => $loggedInUser[static::$settings['id.key']]] : $loggedInUser; if (!isset($credentials[$passKey])) { - static::$settings['AUTH_NO_PASS'] = true; + static::$settings['password'] = true; } if ( - static::$settings['AUTH_NO_PASS'] === false && - static::$settings['PASSWORD_ENCODE'] !== false + static::$settings['password'] === false && + static::$settings['password.encode'] !== false ) { - if (is_callable(static::$settings['PASSWORD_ENCODE'])) { - $credentials[$passKey] = call_user_func(static::$settings['PASSWORD_ENCODE'], $credentials[$passKey]); - } else if (static::$settings['PASSWORD_ENCODE'] === 'md5') { + if (is_callable(static::$settings['password.encode'])) { + $credentials[$passKey] = call_user_func(static::$settings['password.encode'], $credentials[$passKey]); + } else if (static::$settings['password.encode'] === 'md5') { $credentials[$passKey] = md5($credentials[$passKey]); } else { $credentials[$passKey] = Password::hash($credentials[$passKey]); } } - if (static::$settings['USE_TIMESTAMPS']) { - $credentials['updated_at'] = (new \Leaf\Date())->tick()->format(static::$settings['TIMESTAMP_FORMAT']); + if (static::$settings['timestamps']) { + $credentials['updated_at'] = (new \Leaf\Date())->tick()->format(static::$settings['timestamps.format']); } if (count($uniques) > 0) { @@ -307,17 +291,17 @@ public static function update(array $credentials, array $uniques = []) } $token = Authentication::generateSimpleToken( - $user[static::$settings['ID_KEY']], - static::config('TOKEN_SECRET'), - static::config('TOKEN_LIFETIME') + $user[static::$settings['id.key']], + static::config('token.secret'), + static::config('token.lifetime') ); - if (isset($user[static::$settings['ID_KEY']])) { - $userId = $user[static::$settings['ID_KEY']]; + if (isset($user[static::$settings['id.key']])) { + $userId = $user[static::$settings['id.key']]; } - if (static::$settings['HIDE_ID'] && isset($user[static::$settings['ID_KEY']])) { - unset($user[static::$settings['ID_KEY']]); + if (static::$settings['HIDE_ID'] && isset($user[static::$settings['id.key']])) { + unset($user[static::$settings['id.key']]); } if (static::$settings['HIDE_PASSWORD'] && (isset($user[$passKey]) || !$user[$passKey])) { @@ -329,9 +313,9 @@ public static function update(array $credentials, array $uniques = []) return false; } - if (static::config('USE_SESSION')) { + if (static::config('session')) { if (isset($userId)) { - $user[static::$settings['ID_KEY']] = $userId; + $user[static::$settings['id.key']] = $userId; } static::$session->set('AUTH_USER', $user); @@ -355,7 +339,7 @@ public static function update(array $credentials, array $uniques = []) */ public static function useSession() { - static::config('USE_SESSION', true); + static::config('session', true); static::$session = Auth\Session::init(static::config('SESSION_COOKIE_PARAMS')); } @@ -364,8 +348,8 @@ public static function useSession() */ protected static function sessionCheck() { - if (!static::config('USE_SESSION')) { - trigger_error('Turn on USE_SESSION to use this feature.'); + if (!static::config('session')) { + trigger_error('Turn on session to use this feature.'); } if (!static::$session) { @@ -410,15 +394,15 @@ public static function id() { static::leafDbConnect(); - if (static::config('USE_SESSION')) { + if (static::config('session')) { if (static::expireSession()) { return null; } - return static::$session->get('AUTH_USER')[static::$settings['ID_KEY']] ?? null; + return static::$session->get('AUTH_USER')[static::$settings['id.key']] ?? null; } - $payload = static::validateToken(static::config('TOKEN_SECRET')); + $payload = static::validateToken(static::config('token.secret')); return $payload->user_id ?? null; } @@ -430,17 +414,17 @@ public static function id() */ public static function user(array $hidden = []) { - $table = static::$settings['DB_TABLE']; + $table = static::$settings['db.table']; if (!static::id()) { - if (static::config('USE_SESSION')) { + if (static::config('session')) { return static::$session->get('AUTH_USER'); } return null; } - $user = static::$db->select($table)->where(static::$settings['ID_KEY'], static::id())->fetchAssoc(); + $user = static::$db->select($table)->where(static::$settings['id.key'], static::id())->fetchAssoc(); if (count($hidden) > 0) { foreach ($hidden as $item) { diff --git a/src/Auth/Core.php b/src/Auth/Core.php index 28f587b..1efad58 100644 --- a/src/Auth/Core.php +++ b/src/Auth/Core.php @@ -26,31 +26,33 @@ class Core * Auth Settings */ protected static $settings = [ - 'DB_TABLE' => 'users', - 'AUTH_NO_PASS' => false, - 'USE_TIMESTAMPS' => true, - 'TIMESTAMP_FORMAT' => 'c', - 'PASSWORD_ENCODE' => null, - 'PASSWORD_VERIFY' => null, - 'PASSWORD_KEY' => 'password', - 'HIDE_ID' => true, - 'ID_KEY' => 'id', - 'USE_UUID' => false, - 'HIDE_PASSWORD' => true, - 'LOGIN_PARAMS_ERROR' => 'Incorrect credentials!', - 'LOGIN_PASSWORD_ERROR' => 'Password is incorrect!', - 'USE_SESSION' => false, - 'SESSION_ON_REGISTER' => false, - 'GUARD_LOGIN' => '/auth/login', - 'GUARD_REGISTER' => '/auth/register', - 'GUARD_HOME' => '/home', - 'GUARD_LOGOUT' => '/auth/logout', - 'SAVE_SESSION_JWT' => false, - 'TOKEN_LIFETIME' => null, - 'TOKEN_SECRET' => '@_leaf$0Secret!', - 'SESSION_REDIRECT_ON_LOGIN' => true, - 'SESSION_LIFETIME' => self::TIMESTAMP_OF_ONE_DAY, - 'SESSION_COOKIE_PARAMS' => ['secure' => true, 'httponly' => true, 'samesite' => 'lax'], + 'id.key' => 'id', + 'id.uuid' => null, + + 'db.table' => 'users', + + 'timestamps' => true, + 'timestamps.format' => 'c', + + 'password' => true, + 'password.encode' => null, + 'password.verify' => null, + 'password.key' => 'password', + + 'unique' => ['email', 'username'], + 'hidden' => ['field.id', 'field.password'], + + 'session' => false, + 'session.logout' => null, + 'session.register' => null, + 'session.lifetime' => self::TIMESTAMP_OF_ONE_DAY, + 'session.cookie' => ['secure' => true, 'httponly' => true, 'samesite' => 'lax'], + + 'token.lifetime' => null, + 'token.secret' => '@_leaf$0Secret!', + + 'messages.loginParamsError' => 'Incorrect credentials!', + 'messages.loginPasswordError' => 'Password is incorrect!', ]; /** @@ -155,7 +157,7 @@ public static function config($config, $value = null) */ public static function validateUserToken(string $token, ?string $secretKey = null) { - $payload = Authentication::validate($token, $secretKey ?? static::config("TOKEN_SECRET")); + $payload = Authentication::validate($token, $secretKey ?? static::config("token.secret")); if ($payload) return $payload; static::$errors = array_merge(static::$errors, Authentication::errors()); @@ -170,7 +172,7 @@ public static function validateUserToken(string $token, ?string $secretKey = nul */ public static function validateToken(?string $secretKey = null) { - $payload = Authentication::validateToken($secretKey ?? static::config("TOKEN_SECRET")); + $payload = Authentication::validateToken($secretKey ?? static::config("token.secret")); if ($payload) return $payload; static::$errors = array_merge(static::$errors, Authentication::errors()); diff --git a/src/functions.php b/src/functions.php index 555fcbf..597f9c7 100644 --- a/src/functions.php +++ b/src/functions.php @@ -17,35 +17,3 @@ function auth() return \Leaf\Config::get('auth'); } } - -if (!function_exists('guard') && function_exists('auth')) { - /** - * Run an auth guard - * - * @param string $guard The auth guard to run - */ - function guard(string $guard) - { - return auth()->guard($guard); - } -} - -if (!function_exists('hasAuth') && function_exists('auth')) { - /** - * Find out if there's an active sesion - */ - function hasAuth(): bool - { - return !!sessionUser(); - } -} - -if (!function_exists('sessionUser') && function_exists('auth')) { - /** - * Get the currently logged in user - */ - function sessionUser() - { - return \Leaf\Http\Session::get('AUTH_USER'); - } -} diff --git a/tests/AuthTest.php b/tests/AuthTest.php index a16a3d9..660edb5 100644 --- a/tests/AuthTest.php +++ b/tests/AuthTest.php @@ -10,7 +10,7 @@ test('register should save user in database', function () { $auth = new \Leaf\Auth(); - $auth::config(getAuthConfig(['USE_SESSION' => false])); + $auth::config(getAuthConfig(['session' => false])); $response = $auth::register(['username' => 'test-user', 'password' => 'test-password']); expect($response['user']['username'])->toBe('test-user'); diff --git a/tests/Pest.php b/tests/Pest.php index dac93b2..642d828 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -62,7 +62,7 @@ function haveRegisteredUser(string $username, string $password): array \Leaf\Auth\Core::connect(...getConnectionConfig('mysql')); $auth = new \Leaf\Auth(); - $auth::config(getAuthConfig(['USE_SESSION' => false])); + $auth::config(getAuthConfig(['session' => false])); return $auth::register(['username' => $username, 'password' => $password]); } @@ -89,28 +89,28 @@ function getConnectionConfig(?string $dbType = null): array function getAuthConfig(array $settingsReplacement = []): array { $settings = [ - 'DB_TABLE' => 'users', - 'AUTH_NO_PASS' => false, - 'USE_TIMESTAMPS' => false, - 'TIMESTAMP_FORMAT' => 'c', - 'PASSWORD_ENCODE' => null, - 'PASSWORD_VERIFY' => null, - 'PASSWORD_KEY' => 'password', + 'db.table' => 'users', + 'password' => false, + 'timestamps' => false, + 'timestamps.format' => 'c', + 'password.encode' => null, + 'password.verify' => null, + 'password.key' => 'password', 'HIDE_ID' => true, - 'ID_KEY' => 'id', - 'USE_UUID' => false, + 'id.key' => 'id', + 'id.uuid' => false, 'HIDE_PASSWORD' => true, - 'LOGIN_PARAMS_ERROR' => 'Incorrect credentials!', - 'LOGIN_PASSWORD_ERROR' => 'Password is incorrect!', - 'USE_SESSION' => true, + 'messages.loginParamsError' => 'Incorrect credentials!', + 'messages.loginPasswordError' => 'Password is incorrect!', + 'session' => true, 'SESSION_ON_REGISTER' => false, 'GUARD_LOGIN' => '/auth/login', 'GUARD_REGISTER' => '/auth/register', 'GUARD_HOME' => '/home', 'GUARD_LOGOUT' => '/auth/logout', 'SAVE_SESSION_JWT' => false, - 'TOKEN_LIFETIME' => null, - 'TOKEN_SECRET' => '@_leaf$0Secret!', + 'token.lifetime' => null, + 'token.secret' => '@_leaf$0Secret!', 'SESSION_REDIRECT_ON_LOGIN' => false, 'SESSION_LIFETIME' => 60 * 60 * 24, ]; From 5de02ab23a5df0a997b0e282d18991b12548f3dd Mon Sep 17 00:00:00 2001 From: mychidarko Date: Mon, 21 Oct 2024 13:29:03 +0000 Subject: [PATCH 02/32] feat: rewrite auth to match new documentation --- .gitignore | 4 + alchemy.yml | 33 +++++ composer.json | 18 +-- phpunit.xml | 18 --- src/Auth.php | 152 ++++++++++------------- src/Auth/Core.php | 52 +++++--- src/Auth/Session.php | 7 +- src/Helpers/Authentication.php | 220 +++++++++++++++++---------------- src/Helpers/JWT.php | 28 ++--- src/functions.php | 2 +- tests/AuthSessionTest.php | 165 ------------------------- tests/AuthTest.php | 17 --- tests/Pest.php | 126 ++++++++----------- tests/auth.test.php | 152 +++++++++++++++++++++++ tests/extra.test.php | 27 ++++ tests/session.test.php | 183 +++++++++++++++++++++++++++ tests/table-actions.test.php | 92 ++++++++++++++ 17 files changed, 779 insertions(+), 517 deletions(-) create mode 100644 alchemy.yml delete mode 100644 phpunit.xml delete mode 100644 tests/AuthSessionTest.php delete mode 100644 tests/AuthTest.php create mode 100644 tests/auth.test.php create mode 100644 tests/extra.test.php create mode 100644 tests/session.test.php create mode 100644 tests/table-actions.test.php diff --git a/.gitignore b/.gitignore index 55e18db..21cd976 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,7 @@ Thumbs.db # phpstorm .idea/* + +# Alchemy +.alchemy +.phpunit.result.cache diff --git a/alchemy.yml b/alchemy.yml new file mode 100644 index 0000000..3e79ea7 --- /dev/null +++ b/alchemy.yml @@ -0,0 +1,33 @@ +app: + - src + +tests: + engine: pest + parallel: true + paths: + - tests + files: + - '*.test.php' + coverage: + processUncoveredFiles: true + +lint: + preset: PSR12 + rules: + no_unused_imports: true + not_operator_with_successor_space: false + single_quote: true + +actions: + run: + - lint + - tests + os: + - ubuntu-latest + php: + extensions: json, zip, dom, curl, libxml, mbstring + versions: + - '8.3' + events: + - push + - pull_request diff --git a/composer.json b/composer.json index 661f552..280c38d 100644 --- a/composer.json +++ b/composer.json @@ -26,18 +26,19 @@ "Leaf\\": "src" }, "files": [ - "src/functions.php" - ] + "src/functions.php" + ] }, "minimum-stability": "stable", - "prefer-stable": true, + "prefer-stable": true, "require": { "leafs/date": "*", "leafs/password": "*", "leafs/session": "*", "leafs/db": "*", "leafs/form": "*", - "leafs/http": "*" + "leafs/http": "*", + "leafs/alchemy": "dev-next" }, "config": { "allow-plugins": { @@ -45,10 +46,13 @@ } }, "require-dev": { - "leafs/alchemy": "^1.0", - "pestphp/pest": "^1.0 | ^2.0" + "pestphp/pest": "^1.0 | ^2.0", + "friendsofphp/php-cs-fixer": "^3.64" }, "scripts": { - "test": "vendor/bin/pest --colors=always --coverage" + "test": "./vendor/bin/alchemy setup --test", + "alchemy": "./vendor/bin/alchemy setup", + "lint": "./vendor/bin/alchemy setup --lint", + "actions": "./vendor/bin/alchemy setup --actions" } } diff --git a/phpunit.xml b/phpunit.xml deleted file mode 100644 index 8f4b58c..0000000 --- a/phpunit.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - ./tests - - - - - ./app - ./src - - - diff --git a/src/Auth.php b/src/Auth.php index 386100e..aafa745 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -28,6 +28,7 @@ public static function login(array $credentials) { static::leafDbConnect(); + static::$errors = []; $table = static::$settings['db.table']; if (static::config('session')) { @@ -108,10 +109,11 @@ public static function login(array $credentials) * * @return array|false false or all user info + tokens + session data */ - public static function register(array $credentials, array $uniques = []) + public static function register(array $credentials) { static::leafDbConnect(); + static::$errors = []; $table = static::$settings['db.table']; $passKey = static::$settings['password.key']; @@ -123,6 +125,7 @@ public static function register(array $credentials, array $uniques = []) $credentials[$passKey] = (is_callable(static::$settings['password.encode'])) ? call_user_func(static::$settings['password.encode'], $credentials[$passKey]) : Password::hash($credentials[$passKey]); + } if (static::$settings['timestamps']) { @@ -131,14 +134,16 @@ public static function register(array $credentials, array $uniques = []) $credentials['updated_at'] = $now; } - if (static::$settings['id.uuid'] !== false) { - $credentials[static::$settings['id.key']] = call_user_func(static::$settings['id.uuid']); + if (isset($credentials[static::$settings['id.key']])) { + $credentials[static::$settings['id.key']] = is_callable($credentials[static::$settings['id.key']]) + ? call_user_func($credentials[static::$settings['id.key']]) + : $credentials[static::$settings['id.key']]; } try { - $query = static::$db->insert($table)->params($credentials)->unique($uniques)->execute(); + $query = static::$db->insert($table)->params($credentials)->unique(static::$settings['unique'])->execute(); } catch (\Throwable $th) { - trigger_error($th->getMessage()); + throw new \Exception($th->getMessage()); } if (!$query) { @@ -163,11 +168,16 @@ public static function register(array $credentials, array $uniques = []) $userId = $user[static::$settings['id.key']]; } - if (static::$settings['HIDE_ID']) { + if ( + in_array(static::$settings['id.key'], static::$settings['hidden']) || in_array('field.id', static::$settings['hidden']) + ) { unset($user[static::$settings['id.key']]); } - if (static::$settings['HIDE_PASSWORD'] && (isset($user[$passKey]) || !$user[$passKey])) { + if ( + (in_array(static::$settings['password.key'], static::$settings['hidden']) || in_array('field.password', static::$settings['hidden'])) + && (isset($user[$passKey]) || !$user[$passKey]) + ) { unset($user[$passKey]); } @@ -176,20 +186,14 @@ public static function register(array $credentials, array $uniques = []) return false; } - if (static::config('session')) { - if (static::config('SESSION_ON_REGISTER')) { - if (isset($userId)) { - $user[static::$settings['id.key']] = $userId; - } - - self::setUserToSession($user, $token); + if (static::config('session') && static::config('session.register')) { + static::useSession(); - exit(header('location: ' . static::config('GUARD_HOME'))); - } else { - if (static::config('SESSION_REDIRECT_ON_REGISTER')) { - exit(header('location: ' . static::config('GUARD_LOGIN'))); - } + if (isset($userId)) { + $user[static::$settings['id.key']] = $userId; } + + self::setUserToSession($user, $token); } $response['user'] = $user; @@ -206,10 +210,12 @@ public static function register(array $credentials, array $uniques = []) * * @return array|false all user info + tokens + session data */ - public static function update(array $credentials, array $uniques = []) + public static function update(array $credentials) { static::leafDbConnect(); + static::$errors = []; + $table = static::$settings['db.table']; if (static::config('session')) { @@ -227,28 +233,21 @@ public static function update(array $credentials, array $uniques = []) $where = isset($loggedInUser[static::$settings['id.key']]) ? [static::$settings['id.key'] => $loggedInUser[static::$settings['id.key']]] : $loggedInUser; if (!isset($credentials[$passKey])) { - static::$settings['password'] = true; + static::$settings['password'] = false; } - if ( - static::$settings['password'] === false && - static::$settings['password.encode'] !== false - ) { - if (is_callable(static::$settings['password.encode'])) { - $credentials[$passKey] = call_user_func(static::$settings['password.encode'], $credentials[$passKey]); - } else if (static::$settings['password.encode'] === 'md5') { - $credentials[$passKey] = md5($credentials[$passKey]); - } else { - $credentials[$passKey] = Password::hash($credentials[$passKey]); - } + if (static::$settings['password'] && static::$settings['password.encode'] !== false) { + $credentials[$passKey] = (is_callable(static::$settings['password.encode'])) + ? call_user_func(static::$settings['password.encode'], $credentials[$passKey]) + : Password::hash($credentials[$passKey]); } if (static::$settings['timestamps']) { $credentials['updated_at'] = (new \Leaf\Date())->tick()->format(static::$settings['timestamps.format']); } - if (count($uniques) > 0) { - foreach ($uniques as $unique) { + if (count(static::$settings['unique']) > 0) { + foreach (static::$settings['unique'] as $unique) { if (!isset($credentials[$unique])) { trigger_error("$unique not found in credentials."); } @@ -300,11 +299,17 @@ public static function update(array $credentials, array $uniques = []) $userId = $user[static::$settings['id.key']]; } - if (static::$settings['HIDE_ID'] && isset($user[static::$settings['id.key']])) { + if ( + (in_array(static::$settings['id.key'], static::$settings['hidden']) || in_array('field.id', static::$settings['hidden'])) + && isset($user[static::$settings['id.key']]) + ) { unset($user[static::$settings['id.key']]); } - if (static::$settings['HIDE_PASSWORD'] && (isset($user[$passKey]) || !$user[$passKey])) { + if ( + (in_array(static::$settings['password.key'], static::$settings['hidden']) || in_array('field.password', static::$settings['hidden'])) + && (isset($user[$passKey]) || !$user[$passKey]) + ) { unset($user[$passKey]); } @@ -318,14 +323,8 @@ public static function update(array $credentials, array $uniques = []) $user[static::$settings['id.key']] = $userId; } - static::$session->set('AUTH_USER', $user); - static::$session->set('HAS_SESSION', true); - - if (static::config('SAVE_SESSION_JWT')) { - static::$session->set('AUTH_TOKEN', $token); - } - - return $user; + static::$session->set('auth.user', $user); + static::$session->set('auth.token', $token); } $response['user'] = $user; @@ -340,7 +339,7 @@ public static function update(array $credentials, array $uniques = []) public static function useSession() { static::config('session', true); - static::$session = Auth\Session::init(static::config('SESSION_COOKIE_PARAMS')); + static::$session = Auth\Session::init(static::config('session.cookie')); } /** @@ -349,7 +348,7 @@ public static function useSession() protected static function sessionCheck() { if (!static::config('session')) { - trigger_error('Turn on session to use this feature.'); + trigger_error('Turn on sessions to use this feature.'); } if (!static::$session) { @@ -357,25 +356,6 @@ protected static function sessionCheck() } } - /** - * A simple auth guard: 'guest' pages can't be viewed when logged in, - * 'auth' pages can't be viewed without authentication - * - * @param string $type The type of guard/guard options - */ - public static function guard(string $type) - { - static::sessionCheck(); - - if ($type === 'guest' && static::status()) { - exit(header('location: ' . static::config('GUARD_HOME'), true, 302)); - } - - if ($type === 'auth' && !static::status()) { - exit(header('location: ' . static::config('GUARD_LOGIN'), true, 302)); - } - } - /** * Check session status */ @@ -384,7 +364,7 @@ public static function status() static::sessionCheck(); static::expireSession(); - return static::$session->get('AUTH_USER') ?? false; + return static::$session->get('auth.token') ?? false; } /** @@ -394,12 +374,14 @@ public static function id() { static::leafDbConnect(); + static::$errors = []; + if (static::config('session')) { if (static::expireSession()) { return null; } - return static::$session->get('AUTH_USER')[static::$settings['id.key']] ?? null; + return static::$session->get('auth.token')[static::$settings['id.key']] ?? null; } $payload = static::validateToken(static::config('token.secret')); @@ -417,11 +399,7 @@ public static function user(array $hidden = []) $table = static::$settings['db.table']; if (!static::id()) { - if (static::config('session')) { - return static::$session->get('AUTH_USER'); - } - - return null; + return (static::config('session')) ? static::$session->get('auth.token') : null; } $user = static::$db->select($table)->where(static::$settings['id.key'], static::id())->fetchAssoc(); @@ -463,7 +441,7 @@ private static function expireSession(): bool { self::sessionCheck(); - $sessionTtl = static::$session->get('SESSION_TTL'); + $sessionTtl = static::$session->get('session.ttl'); if (!$sessionTtl) { return false; @@ -472,12 +450,12 @@ private static function expireSession(): bool $isSessionExpired = time() > $sessionTtl; if ($isSessionExpired) { - static::$session->unset('AUTH_USER'); + static::$session->unset('auth.token'); static::$session->unset('HAS_SESSION'); - static::$session->unset('AUTH_TOKEN'); - static::$session->unset('SESSION_STARTED_AT'); - static::$session->unset('SESSION_LAST_ACTIVITY'); - static::$session->unset('SESSION_TTL'); + static::$session->unset('auth.token'); + static::$session->unset('session.startedAt'); + static::$session->unset('session.lastActivity'); + static::$session->unset('session.ttl'); } return $isSessionExpired; @@ -490,7 +468,7 @@ public static function lastActive() { static::sessionCheck(); - return time() - static::$session->get('SESSION_LAST_ACTIVITY'); + return time() - static::$session->get('session.lastActivity'); } /** @@ -504,8 +482,8 @@ public static function refresh(bool $clearData = true) $success = static::$session->regenerate($clearData); - static::$session->set('SESSION_STARTED_AT', time()); - static::$session->set('SESSION_LAST_ACTIVITY', time()); + static::$session->set('session.startedAt', time()); + static::$session->set('session.lastActivity', time()); static::setSessionTtl(); return $success; @@ -518,7 +496,7 @@ public static function length() { static::sessionCheck(); - return time() - static::$session->get('SESSION_STARTED_AT'); + return time() - static::$session->get('session.startedAt'); } /** @@ -531,12 +509,12 @@ private static function setUserToSession(array $user, string $token): void { session_regenerate_id(); - static::$session->set('AUTH_USER', $user); + static::$session->set('auth.token', $user); static::$session->set('HAS_SESSION', true); static::setSessionTtl(); if (static::config('SAVE_SESSION_JWT')) { - static::$session->set('AUTH_TOKEN', $token); + static::$session->set('auth.token', $token); } } @@ -545,14 +523,14 @@ private static function setUserToSession(array $user, string $token): void */ private static function setSessionTtl(): void { - $sessionLifetime = static::config('SESSION_LIFETIME'); + $sessionLifetime = static::config('session.lifetime'); if ($sessionLifetime === 0) { return; } if (is_int($sessionLifetime)) { - static::$session->set('SESSION_TTL', time() + $sessionLifetime); + static::$session->set('session.ttl', time() + $sessionLifetime); return; } @@ -562,6 +540,6 @@ private static function setSessionTtl(): void throw new \Exception('Provided string could not be converted to time'); } - static::$session->set('SESSION_TTL', $sessionLifetimeInTime); + static::$session->set('session.ttl', $sessionLifetimeInTime); } } diff --git a/src/Auth/Core.php b/src/Auth/Core.php index 1efad58..95fe009 100644 --- a/src/Auth/Core.php +++ b/src/Auth/Core.php @@ -27,8 +27,6 @@ class Core */ protected static $settings = [ 'id.key' => 'id', - 'id.uuid' => null, - 'db.table' => 'users', 'timestamps' => true, @@ -45,9 +43,9 @@ class Core 'session' => false, 'session.logout' => null, 'session.register' => null, - 'session.lifetime' => self::TIMESTAMP_OF_ONE_DAY, + 'session.lifetime' => 60 * 60 * 24, 'session.cookie' => ['secure' => true, 'httponly' => true, 'samesite' => 'lax'], - + 'token.lifetime' => null, 'token.secret' => '@_leaf$0Secret!', @@ -77,14 +75,20 @@ class Core */ public static function connect( $host, - string $dbname, - string $user, - string $password, - string $dbtype, + string $dbname = null, + string $user = null, + string $password = null, + string $dbtype = null, array $pdoOptions = [] ) { $db = new \Leaf\Db(); - $db->connect($host, $dbname, $user, $password, $dbtype, $pdoOptions); + + if (is_array($host)) { + $db->connect($host); + } else { + $db->connect($host, $dbname, $user, $password, $dbtype, $pdoOptions); + } + static::$db = $db; } @@ -97,6 +101,7 @@ public static function autoConnect(array $pdoOptions = []) { $db = new \Leaf\Db(); $db->autoConnect($pdoOptions); + static::$db = $db; } @@ -109,6 +114,7 @@ public static function dbConnection(\PDO $connection) { $db = new \Leaf\Db(); $db->connection($connection); + static::$db = $db; } @@ -157,12 +163,14 @@ public static function config($config, $value = null) */ public static function validateUserToken(string $token, ?string $secretKey = null) { - $payload = Authentication::validate($token, $secretKey ?? static::config("token.secret")); - if ($payload) return $payload; + $payload = Authentication::validate($token, $secretKey ?? static::config('token.secret')); - static::$errors = array_merge(static::$errors, Authentication::errors()); + if (!$payload) { + static::$errors = array_merge(static::$errors, Authentication::errors()); + return null; + } - return null; + return $payload; } /** @@ -172,12 +180,14 @@ public static function validateUserToken(string $token, ?string $secretKey = nul */ public static function validateToken(?string $secretKey = null) { - $payload = Authentication::validateToken($secretKey ?? static::config("token.secret")); - if ($payload) return $payload; + $payload = Authentication::validateToken($secretKey ?? static::config('token.secret')); - static::$errors = array_merge(static::$errors, Authentication::errors()); + if (!$payload) { + static::$errors = array_merge(static::$errors, Authentication::errors()); + return null; + } - return null; + return $payload; } /** @@ -186,11 +196,13 @@ public static function validateToken(?string $secretKey = null) public static function getBearerToken() { $token = Authentication::getBearerToken(); - if ($token) return $token; - static::$errors = array_merge(static::$errors, Authentication::errors()); + if (!$token) { + static::$errors = array_merge(static::$errors, Authentication::errors()); + return null; + } - return null; + return $token; } /** diff --git a/src/Auth/Session.php b/src/Auth/Session.php index e8da0d7..a80b735 100644 --- a/src/Auth/Session.php +++ b/src/Auth/Session.php @@ -3,6 +3,7 @@ declare(strict_types=1); namespace Leaf\Auth; + /** * Auth Sessions [CORE] * ----- @@ -28,11 +29,11 @@ public static function init(array $sessionCookieParams = []) session_start(); }; - if (!static::$session->get("SESSION_STARTED_AT")) { - static::$session->set("SESSION_STARTED_AT", time()); + if (!static::$session->get('session.startedAt')) { + static::$session->set('session.startedAt', time()); } - static::$session->set("SESSION_LAST_ACTIVITY", time()); + static::$session->set('session.lastActivity', time()); return static::$session; } diff --git a/src/Helpers/Authentication.php b/src/Helpers/Authentication.php index 5df9384..6cd01f4 100755 --- a/src/Helpers/Authentication.php +++ b/src/Helpers/Authentication.php @@ -6,37 +6,37 @@ * Leaf Authentication * --------------------------------------------- * Authentication helper for Leaf PHP - * + * * @author Michael Darko * @since v1.2.0 */ class Authentication { - /** - * Any errors caught - */ - protected static $errorsArray = []; - - /** - * Quickly generate a JWT encoding a user id - * - * @param string $userId The user id to encode - * @param string $secretPhrase The user id to encode - * @param int $expiresAt Token lifetime - * - * @return string The generated token - */ - public static function generateSimpleToken(string $userId, string $secretPhrase, int $expiresAt = null): string + /** + * Any errors caught + */ + protected static $errorsArray = []; + + /** + * Quickly generate a JWT encoding a user id + * + * @param string $userId The user id to encode + * @param string $secretPhrase The user id to encode + * @param int $expiresAt Token lifetime + * + * @return string The generated token + */ + public static function generateSimpleToken(string $userId, string $secretPhrase, int $expiresAt = null): string { - $payload = [ - 'iat' => time(), - 'iss' => 'localhost', - 'exp' => time() + ($expiresAt ?? (60 * 60 * 24)), - 'user_id' => $userId - ]; + $payload = [ + 'iat' => time(), + 'iss' => 'localhost', + 'exp' => time() + ($expiresAt ?? (60 * 60 * 24)), + 'user_id' => $userId + ]; - return self::generateToken($payload, $secretPhrase); - } + return self::generateToken($payload, $secretPhrase); + } /** * Create a JWT with your own payload @@ -46,89 +46,93 @@ public static function generateSimpleToken(string $userId, string $secretPhrase, * * @return string The generated token */ - public static function generateToken(array $payload, string $secretPhrase): string + public static function generateToken(array $payload, string $secretPhrase): string + { + return JWT::encode($payload, $secretPhrase); + } + + /** + * Get Authorization Headers + */ + public static function getAuthorizationHeader() + { + $headers = null; + + if (isset($_SERVER['Authorization'])) { + $headers = trim($_SERVER['Authorization']); + } elseif (isset($_SERVER['HTTP_AUTHORIZATION'])) { + $headers = trim($_SERVER['HTTP_AUTHORIZATION']); + } elseif (function_exists('apache_request_headers')) { + $requestHeaders = apache_request_headers(); + // Server-side fix for bug in old Android versions (a nice side-effect of this fix means we don't care about capitalization for Authorization) + $requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders)); + + if (isset($requestHeaders['Authorization'])) { + $headers = trim($requestHeaders['Authorization']); + } + } + + return $headers; + } + + /** + * get access token from header + */ + public static function getBearerToken() + { + $headers = self::getAuthorizationHeader(); + + if (!empty($headers)) { + if (preg_match('/Bearer\s(\S+)/', $headers, $matches)) { + return $matches[1]; + } + + self::$errorsArray['token'] = 'Access token not found'; + return null; + } + + self::$errorsArray['token'] = 'Access token not found'; + return null; + } + + /** + * Validate and decode access token in header + */ + public static function validateToken($secretPhrase) + { + $bearerToken = self::getBearerToken(); + if ($bearerToken === null) { + return null; + } + + return self::validate($bearerToken, $secretPhrase); + } + + /** + * Validate access token + * + * @param string $token Access token to validate and decode + */ + public static function validate($token, $secretPhrase) + { + try { + $payload = JWT::decode($token, $secretPhrase, ['HS256']); + if ($payload !== null) { + return $payload; + } + } catch (\DomainException $exception) { + self::$errorsArray['token'] = 'Malformed token'; + } + + self::$errorsArray = array_merge(self::$errorsArray, JWT::errors()); + return null; + } + + /** + * Get all authentication errors as associative array + */ + public static function errors() { - return JWT::encode($payload, $secretPhrase); - } - - /** - * Get Authorization Headers - */ - public static function getAuthorizationHeader() - { - $headers = null; - - if (isset($_SERVER['Authorization'])) { - $headers = trim($_SERVER["Authorization"]); - } else if (isset($_SERVER['HTTP_AUTHORIZATION'])) { - $headers = trim($_SERVER["HTTP_AUTHORIZATION"]); - } else if (function_exists('apache_request_headers')) { - $requestHeaders = apache_request_headers(); - // Server-side fix for bug in old Android versions (a nice side-effect of this fix means we don't care about capitalization for Authorization) - $requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders)); - - if (isset($requestHeaders['Authorization'])) { - $headers = trim($requestHeaders['Authorization']); - } - } - - return $headers; - } - - /** - * get access token from header - */ - public static function getBearerToken() - { - $headers = self::getAuthorizationHeader(); - - if (!empty($headers)) { - if (preg_match('/Bearer\s(\S+)/', $headers, $matches)) { - return $matches[1]; - } - - self::$errorsArray["token"] = "Access token not found"; - return null; - } - - self::$errorsArray["token"] = "Access token not found"; - return null; - } - - /** - * Validate and decode access token in header - */ - public static function validateToken($secretPhrase) - { - $bearerToken = self::getBearerToken(); - if ($bearerToken === null) return null; - - return self::validate($bearerToken, $secretPhrase); - } - - /** - * Validate access token - * - * @param string $token Access token to validate and decode - */ - public static function validate($token, $secretPhrase) - { - try { - $payload = JWT::decode($token, $secretPhrase, ['HS256']); - if ($payload !== null) return $payload; - } catch (\DomainException $exception) { - self::$errorsArray["token"] = "Malformed token"; - } - - self::$errorsArray = array_merge(self::$errorsArray, JWT::errors()); - return null; - } - - /** - * Get all authentication errors as associative array - */ - public static function errors() - { - return self::$errorsArray; - } + return self::$errorsArray; + } } diff --git a/src/Helpers/JWT.php b/src/Helpers/JWT.php index 8fe1da5..fe48a98 100755 --- a/src/Helpers/JWT.php +++ b/src/Helpers/JWT.php @@ -60,30 +60,30 @@ public static function decode($jwt, $key, array $allowed_algs = array()) { $timestamp = is_null(static::$timestamp) ? time() : static::$timestamp; if (empty($key)) { - return static::saveErr("Key may not be empty"); + return static::saveErr('Key may not be empty'); } $tks = explode('.', $jwt); if (count($tks) != 3) { - return static::saveErr("Wrong number of segments"); + return static::saveErr('Wrong number of segments'); } list($headb64, $bodyb64, $cryptob64) = $tks; if (null === ($header = static::jsonDecode(static::urlsafeB64Decode($headb64)))) { - return static::saveErr("Invalid header encoding"); + return static::saveErr('Invalid header encoding'); } if (null === $payload = static::jsonDecode(static::urlsafeB64Decode($bodyb64))) { - return static::saveErr("Invalid claims encoding"); + return static::saveErr('Invalid claims encoding'); } if (false === ($sig = static::urlsafeB64Decode($cryptob64))) { - return static::saveErr("Invalid signature encoding"); + return static::saveErr('Invalid signature encoding'); } if (empty($header->alg)) { - return static::saveErr("Empty algorithm"); + return static::saveErr('Empty algorithm'); } if (empty(static::$supported_algs[$header->alg])) { - return static::saveErr("Algorithm not supported"); + return static::saveErr('Algorithm not supported'); } if (!in_array($header->alg, $allowed_algs)) { - return static::saveErr("Algorithm not allowed"); + return static::saveErr('Algorithm not allowed'); } if (is_array($key) || $key instanceof \ArrayAccess) { if (isset($header->kid)) { @@ -97,13 +97,13 @@ public static function decode($jwt, $key, array $allowed_algs = array()) } // Check the signature if (!static::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) { - return static::saveErr("Signature verification failed"); + return static::saveErr('Signature verification failed'); } // Check if the nbf if it is defined. This is the time that the // token can actually be used. If it's not yet that time, abort. if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) { return static::saveErr( - "Cannot handle token prior to " . date(\DateTime::ISO8601, $payload->nbf) + 'Cannot handle token prior to ' . date(\DateTime::ISO8601, $payload->nbf) ); } // Check that this token has been created before "now". This prevents @@ -111,12 +111,12 @@ public static function decode($jwt, $key, array $allowed_algs = array()) // correctly used the nbf claim). if (isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) { return static::saveErr( - "Cannot handle token prior to " . date(\DateTime::ISO8601, $payload->iat) + 'Cannot handle token prior to ' . date(\DateTime::ISO8601, $payload->iat) ); } // Check if this token has expired. if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) { - return static::saveErr("Expired token"); + return static::saveErr('Expired token'); } return $payload; } @@ -180,7 +180,7 @@ public static function sign($msg, $key, $alg = 'HS256') $signature = ''; $success = openssl_sign($msg, $signature, $key, $algorithm); if (!$success) { - throw new \DomainException("OpenSSL unable to sign data"); + throw new \DomainException('OpenSSL unable to sign data'); } else { return $signature; } @@ -355,7 +355,7 @@ private static function safeStrlen($str) return strlen($str); } - protected static function saveErr($err, $key = "token") + protected static function saveErr($err, $key = 'token') { self::$errorsArray[$key] = $err; return null; diff --git a/src/functions.php b/src/functions.php index 597f9c7..a88c508 100644 --- a/src/functions.php +++ b/src/functions.php @@ -3,7 +3,7 @@ if (!function_exists('auth') && class_exists('Leaf\App')) { /** * Return the leaf auth object - * + * * @return Leaf\Auth */ function auth() diff --git a/tests/AuthSessionTest.php b/tests/AuthSessionTest.php deleted file mode 100644 index 54224ef..0000000 --- a/tests/AuthSessionTest.php +++ /dev/null @@ -1,165 +0,0 @@ - 'login-user', 'password' => 'login-pass']); - - $session = new \Leaf\Http\Session(false); - $user = $session->get('AUTH_USER'); - - expect($user['username'])->toBe('login-user'); -}); - -test('login should set session ttl', function () { - $auth = new \Leaf\Auth(); - $auth::config(getAuthConfig()); - - $timeBeforeLogin = time(); - $auth::login(['username' => 'login-user', 'password' => 'login-pass']); - - $session = new \Leaf\Http\Session(false); - $sessionTtl = $session->get('SESSION_TTL'); - - expect($sessionTtl > $timeBeforeLogin)->toBeTrue(); -}); - -test('login should set regenerate session id', function () { - $auth = new \Leaf\Auth(); - $auth::config(getAuthConfig()); - - $auth::login(['username' => 'login-user', 'password' => 'login-pass']); - - $originalSessionId = session_id(); - - $auth::login(['username' => 'login-user', 'password' => 'login-pass']); - - expect(session_id())->not()->toBe($originalSessionId); -}); - -test('login should set secure session cookie params', function () { - $auth = new \Leaf\Auth(); - $auth::config(getAuthConfig()); - - $auth::login(['username' => 'login-user', 'password' => 'login-pass']); - - $cookieParams = session_get_cookie_params(); - - expect($cookieParams['secure'])->toBeTrue(); - expect($cookieParams['httponly'])->toBeTrue(); - expect($cookieParams['samesite'])->toBe('lax'); -}); - -test('register should set session ttl on login', function () { - $auth = new \Leaf\Auth(); - $auth::config(getAuthConfig()); - - $timeBeforeLogin = time(); - $auth::login(['username' => 'login-user', 'password' => 'login-pass']); - - $session = new \Leaf\Http\Session(false); - $sessionTtl = $session->get('SESSION_TTL'); - - expect($sessionTtl > $timeBeforeLogin)->toBeTrue(); -}); - -test('Session should expire when fetching user, and then login is possible again', function () { - $auth = new \Leaf\Auth(); - $auth::config(getAuthConfig(['SESSION_LIFETIME' => 2])); - - $auth::login(['username' => 'login-user', 'password' => 'login-pass']); - - $user = $auth::user(); - expect($user)->not()->toBeNull(); - expect($user['username'])->toBe('login-user'); - - sleep(1); - expect($auth::user())->not()->toBeNull(); - - sleep(2); - expect($auth::user())->toBeNull(); - - $userAfterReLogin = $auth::login(['username' => 'login-user', 'password' => 'login-pass']); - expect($userAfterReLogin)->not()->toBeNull(); - expect($userAfterReLogin['user']['username'])->toBe('login-user'); -}); - -test('Session should not expire when fetching user if session lifetime is 0', function () { - $auth = new \Leaf\Auth(); - $auth::config(getAuthConfig(['SESSION_LIFETIME' => 0])); - - $auth::login(['username' => 'login-user', 'password' => 'login-pass']); - - $user = $auth::user(); - expect($user)->not()->toBeNull(); - expect($user['username'])->toBe('login-user'); - - sleep(2); - expect($auth::user())->not()->toBeNull(); -}); - -test('Session should expire when fetching user id', function () { - $auth = new \Leaf\Auth(); - $auth::config(getAuthConfig(['SESSION_LIFETIME' => 2])); - - $auth::login(['username' => 'login-user', 'password' => 'login-pass']); - - expect($auth::id())->not()->toBeNull(); - - sleep(1); - expect($auth::id())->not()->toBeNull(); - - sleep(2); - expect($auth::id())->toBeNull(); -}); - -test('Session should expire when fetching status', function () { - $auth = new \Leaf\Auth(); - $auth::config(getAuthConfig(['SESSION_LIFETIME' => 2])); - $auth::login(['username' => 'login-user', 'password' => 'login-pass']); - - expect($auth::status())->not()->toBeNull(); - - sleep(1); - expect($auth::status())->not()->toBeNull(); - - sleep(2); - expect($auth::status())->toBeFalse(); -}); - -test('Session lifetime should set correct session ttl when string is configured instead of timestamp', function () { - $auth = new \Leaf\Auth(); - $auth::config(getAuthConfig(['SESSION_LIFETIME' => '1 day'])); - $auth::login(['username' => 'login-user', 'password' => 'login-pass']); - - expect($auth::status())->not()->toBeNull(); - - $timestampOneDay = 60 * 60 * 24; - $session = new \Leaf\Http\Session(false); - $sessionTtl = $session->get('SESSION_TTL'); - - expect($sessionTtl)->toBe(time() + $timestampOneDay); -}); - -test('Login should throw error when lifetime string is invalid', function () { - $auth = new \Leaf\Auth(); - $auth::config(getAuthConfig(['SESSION_LIFETIME' => 'invalid string'])); - - expect(fn() => $auth::login(['username' => 'login-user', 'password' => 'login-pass'])) - ->toThrow(Exception::class, 'Provided string could not be converted to time'); -}); diff --git a/tests/AuthTest.php b/tests/AuthTest.php deleted file mode 100644 index 660edb5..0000000 --- a/tests/AuthTest.php +++ /dev/null @@ -1,17 +0,0 @@ - false])); - $response = $auth::register(['username' => 'test-user', 'password' => 'test-password']); - - expect($response['user']['username'])->toBe('test-user'); -}); diff --git a/tests/Pest.php b/tests/Pest.php index 642d828..e3ca842 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,118 +1,90 @@ in('Feature'); - -/* -|-------------------------------------------------------------------------- -| Expectations -|-------------------------------------------------------------------------- -| -| When you're writing tests, you often need to check that values meet certain conditions. The -| "expect()" function gives you access to a set of "expectations" methods that you can use -| to assert different things. Of course, you may extend the Expectation API at any time. -| -*/ - -expect()->extend('toBeOne', function () { - return $this->toBe(1); -}); - -/* -|-------------------------------------------------------------------------- -| Functions -|-------------------------------------------------------------------------- -| -| While Pest is very powerful out-of-the-box, you may have some testing code specific to your -| project that you don't want to repeat in every file. Here you can also expose helpers as -| global functions to help you to reduce the number of lines of code in your test files. -| -*/ - -function createUsersTable() +function createUsersTable($table = 'users', $dynamicId = false) { $db = new \Leaf\Db(); - $db->connect(...getConnectionConfig()); + $db->connect(getConnectionConfig()); + + // $auth = new \Leaf\Auth(); + // $auth->dbConnection($db->connection()); $db->createTableIfNotExists( - 'users', + $table, [ - 'id' => 'int NOT NULL AUTO_INCREMENT', + // using varchar(255) to mimic binary(16) for uuid + 'id' => $dynamicId ? 'varchar(255)' : 'int NOT NULL AUTO_INCREMENT', 'username' => 'varchar(255)', + 'email' => 'varchar(255)', 'password' => 'varchar(255)', 'created_at' => 'datetime', 'updated_at' => 'datetime', 'PRIMARY KEY' => '(id)', ] )->execute(); + + $db->close(); } -function haveRegisteredUser(string $username, string $password): array +function deleteUser(string $username, $table = 'users') { - \Leaf\Auth\Core::connect(...getConnectionConfig('mysql')); - - $auth = new \Leaf\Auth(); - $auth::config(getAuthConfig(['session' => false])); + $db = new \Leaf\Db(); + $db->connect(getConnectionConfig()); - return $auth::register(['username' => $username, 'password' => $password]); + $db->delete($table)->where('username', $username)->execute(); } -function deleteUser(string $username) +function getConnectionConfig(): array { - $db = new \Leaf\Db(); - $db->connect(...getConnectionConfig()); - - $db->delete('users')->where('username', '=', $username)->execute(); + return [ + 'port' => '3306', + 'host' => '127.0.0.1', + 'username' => 'root', + 'password' => '', + 'dbname' => 'atest', + ]; } -function getConnectionConfig(?string $dbType = null): array +function auth(): \Leaf\Auth { - $config = ['localhost', 'leaf', 'root', 'root']; + $db = new \Leaf\Db(); + $db->connect(getConnectionConfig()); - if ($dbType) { - $config[] = $dbType; - } + $auth = new \Leaf\Auth(); + $auth->dbConnection($db->connection()); - return $config; + return $auth; } function getAuthConfig(array $settingsReplacement = []): array { $settings = [ + 'id.key' => 'id', + 'id.uuid' => null, + 'db.table' => 'users', - 'password' => false, - 'timestamps' => false, + + 'timestamps' => true, 'timestamps.format' => 'c', + + 'password' => true, 'password.encode' => null, 'password.verify' => null, 'password.key' => 'password', - 'HIDE_ID' => true, - 'id.key' => 'id', - 'id.uuid' => false, - 'HIDE_PASSWORD' => true, - 'messages.loginParamsError' => 'Incorrect credentials!', - 'messages.loginPasswordError' => 'Password is incorrect!', - 'session' => true, - 'SESSION_ON_REGISTER' => false, - 'GUARD_LOGIN' => '/auth/login', - 'GUARD_REGISTER' => '/auth/register', - 'GUARD_HOME' => '/home', - 'GUARD_LOGOUT' => '/auth/logout', - 'SAVE_SESSION_JWT' => false, + + 'unique' => ['email', 'username'], + 'hidden' => ['field.id', 'field.password'], + + 'session' => false, + 'session.logout' => null, + 'session.register' => null, + 'session.lifetime' => 60 * 60 * 24, + 'session.cookie' => ['secure' => true, 'httponly' => true, 'samesite' => 'lax'], + 'token.lifetime' => null, 'token.secret' => '@_leaf$0Secret!', - 'SESSION_REDIRECT_ON_LOGIN' => false, - 'SESSION_LIFETIME' => 60 * 60 * 24, + + 'messages.loginParamsError' => 'Incorrect credentials!', + 'messages.loginPasswordError' => 'Password is incorrect!', ]; return array_replace($settings, $settingsReplacement); diff --git a/tests/auth.test.php b/tests/auth.test.php new file mode 100644 index 0000000..0ccfcad --- /dev/null +++ b/tests/auth.test.php @@ -0,0 +1,152 @@ +config(['timestamps.format' => 'YYYY-MM-DD HH:MM:ss']); + + $response = $auth->register([ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password', + ]); + + if (!$response) { + $this->fail(json_encode($auth->errors())); + } + + expect($response['user']['username'])->toBe('test-user'); +}); + +test('register should fail if user already exists', function () { + $auth = auth(); + + $auth->config([ + 'timestamps.format' => 'YYYY-MM-DD HH:MM:ss', + 'unique' => ['username', 'email'] + ]); + + $response = $auth->register([ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password' + ]); + + expect($response)->toBe(false); + expect($auth->errors()['email'])->toBe('email already exists'); + expect($auth->errors()['username'])->toBe('username already exists'); +}); + +test('login should retrieve user from database', function () { + $auth = auth(); + + $response = $auth->login([ + 'username' => 'test-user', + 'password' => 'password' + ]); + + if (!$response) { + $this->fail(json_encode($auth->errors())); + } + + expect($response['user']['username'])->toBe('test-user'); +}); + +test('login should fail if user does not exist', function () { + $auth = auth(); + + $response = $auth->login([ + 'username' => 'non-existent-user', + 'password' => 'password' + ]); + + expect($response)->toBe(false); + expect($auth->errors()['auth'])->toBe('Incorrect credentials!'); +}); + +test('login should fail if password is wrong', function () { + $db = new \Leaf\Db(); + $db->connect(getConnectionConfig()); + + $db + ->insert('users') + ->params([ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => '$2y$10$91T2Y5/D4e9QXw8EgU33E.9J1N23hHg.6lG5ofVhh69la492kqKga', + ]) + ->execute(); + + $auth = new \Leaf\Auth(); + $auth->dbConnection($db->connection()); + + $userData = $auth->login([ + 'username' => 'test-user', + 'password' => 'wrong-password' + ]); + + expect($userData)->toBe(false); + expect($auth->errors()['password'])->toBe('Password is incorrect!'); +}); + +test('update should update user in database', function () { + $auth = auth(); + + $auth->useSession(); + + $data = $auth->login([ + 'username' => 'test-user', + 'password' => 'password' + ]); + + if (!$data) { + $this->fail(json_encode($auth->errors())); + } + + $response = $auth->update([ + 'username' => 'test-user22', + 'email' => 'test-user22@test.com', + ]); + + if (!$response) { + $this->fail(json_encode($auth->errors())); + } + + $auth->config(['session' => false]); + + expect($response['user']['username'])->toBe('test-user22'); + expect($response['user']['email'])->toBe('test-user22@test.com'); +}); + +test('update should fail if user already exists', function () { + $auth = auth(); + + $auth->useSession(); + + $data = $auth->login([ + 'username' => 'test-user22', + 'password' => 'password' + ]); + + if (!$data) { + $this->fail(json_encode($auth->errors())); + } + + $response = $auth->update([ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + ]); + + expect($response)->toBe(false); + expect($auth->errors()['email'])->toBe('email already exists'); + expect($auth->errors()['username'])->toBe('username already exists'); +}); diff --git a/tests/extra.test.php b/tests/extra.test.php new file mode 100644 index 0000000..ff33dca --- /dev/null +++ b/tests/extra.test.php @@ -0,0 +1,27 @@ +config(['timestamps.format' => 'YYYY-MM-DD HH:MM:ss']); + + $auth->register([ + 'username' => 'extra-user', + 'email' => 'extra-user@example.com', + 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', + ]); +}); + +afterAll(function () { + deleteUser('extra-user'); +}); + +test('login should produce valid token', function () { + $auth = auth(); + + $auth->config(['hidden' => ['password']]); + $data = $auth->login(['username' => 'extra-user', 'password' => 'login-pass']); + + expect($auth->validateUserToken($data['token'])->user_id)->toBe((string) $data['user']['id']); +}); diff --git a/tests/session.test.php b/tests/session.test.php new file mode 100644 index 0000000..cd331f6 --- /dev/null +++ b/tests/session.test.php @@ -0,0 +1,183 @@ +config(['timestamps.format' => 'YYYY-MM-DD HH:MM:ss']); + + $auth->register([ + 'username' => 'login-user', + 'email' => 'login-user@example.com', + 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', + ]); +}); + +afterEach(function () { + if (!session_status()) { + session_start(); + } + + session_destroy(); +}); + +afterAll(function () { + deleteUser('login-user'); +}); + +test('login should set user session', function () { + $auth = auth(); + $auth->useSession(); + + $auth->login(['username' => 'login-user', 'password' => 'login-pass']); + + $session = new \Leaf\Http\Session(false); + $user = $session->get('auth.token'); + + expect($user['username'])->toBe('login-user'); +}); + +test('login should set session ttl', function () { + $auth = auth(); + $auth->useSession(); + + $timeBeforeLogin = time(); + $auth->login(['username' => 'login-user', 'password' => 'login-pass']); + + $session = new \Leaf\Http\Session(false); + $sessionTtl = $session->get('session.ttl'); + + expect($sessionTtl > $timeBeforeLogin)->toBeTrue(); +}); + +test('login should set regenerate session id', function () { + $auth = auth(); + $auth->useSession(); + + $auth->login(['username' => 'login-user', 'password' => 'login-pass']); + + $originalSessionId = session_id(); + + $auth->login(['username' => 'login-user', 'password' => 'login-pass']); + + expect(session_id())->not()->toBe($originalSessionId); +}); + +test('login should set secure session cookie params', function () { + $auth = auth(); + $auth->useSession(); + + $auth->login(['username' => 'login-user', 'password' => 'login-pass']); + + $cookieParams = session_get_cookie_params(); + + expect($cookieParams['secure'])->toBeTrue(); + expect($cookieParams['httponly'])->toBeTrue(); + expect($cookieParams['samesite'])->toBe('lax'); +}); + +test('Session should expire when fetching user, and then login is possible again', function () { + $auth = new \Leaf\Auth(); + $auth->config(['session.lifetime' => 2]); + + $auth->login(['username' => 'login-user', 'password' => 'login-pass']); + + $user = $auth->user(); + + expect($user)->not()->toBeNull(); + expect($user['username'])->toBe('login-user'); + + sleep(1); + expect($auth->user())->not()->toBeNull(); + + sleep(2); + expect($auth->user())->toBeNull(); + + $userAfterReLogin = $auth->login(['username' => 'login-user', 'password' => 'login-pass']); + + expect($userAfterReLogin)->not()->toBeNull(); + expect($userAfterReLogin['user']['username'])->toBe('login-user'); +}); + +test('Session should not expire when fetching user if session lifetime is 0', function () { + $auth = new \Leaf\Auth(); + $auth->config(['session.lifetime' => 0]); + + $auth->login(['username' => 'login-user', 'password' => 'login-pass']); + + $user = $auth->user(); + + expect($user)->not()->toBeNull(); + expect($user['username'])->toBe('login-user'); + + sleep(2); + expect($auth->user())->not()->toBeNull(); +}); + +test('Session should expire when fetching user id', function () { + $auth = new \Leaf\Auth(); + + $auth->config(['session.lifetime' => 2]); + $auth->login(['username' => 'login-user', 'password' => 'login-pass']); + + expect($auth->id())->not()->toBeNull(); + + sleep(1); + expect($auth->id())->not()->toBeNull(); + + sleep(2); + expect($auth->id())->toBeNull(); +}); + +test('Session should expire when fetching status', function () { + $auth = new \Leaf\Auth(); + + $auth->config(['session.lifetime' => 2]); + $auth->login(['username' => 'login-user', 'password' => 'login-pass']); + + expect($auth->status())->not()->toBeNull(); + + sleep(1); + expect($auth->status())->not()->toBeNull(); + + sleep(2); + expect($auth->status())->toBeFalse(); +}); + +test('Session lifetime should set correct session ttl when string is configured instead of timestamp', function () { + $auth = new \Leaf\Auth(); + + $auth->config(['session.lifetime' => '1 day']); + $auth->login(['username' => 'login-user', 'password' => 'login-pass']); + + expect($auth->status())->not()->toBeNull(); + + $timestampOneDay = 60 * 60 * 24; + $session = new \Leaf\Http\Session(false); + $sessionTtl = $session->get('session.ttl'); + + expect($sessionTtl)->toBe(time() + $timestampOneDay); +}); + +test('Login should throw error when lifetime string is invalid', function () { + $auth = new \Leaf\Auth(); + $auth->config(['session.lifetime' => 'invalid string']); + + expect(function () use ($auth) { + return $auth->login(['username' => 'login-user', 'password' => 'login-pass']); + })->toThrow(Exception::class, 'Provided string could not be converted to time'); +}); + +test('Login should set session ttl on login', function () { + $auth = auth(); + $auth->useSession(); + $auth->config(['session.lifetime' => 2]); + + $timeBeforeLogin = time(); + $auth->login(['username' => 'login-user', 'password' => 'login-pass']); + + $session = new \Leaf\Http\Session(false); + $sessionTtl = $session->get('session.ttl'); + + expect($sessionTtl > $timeBeforeLogin)->toBeTrue(); +}); diff --git a/tests/table-actions.test.php b/tests/table-actions.test.php new file mode 100644 index 0000000..a99c7f7 --- /dev/null +++ b/tests/table-actions.test.php @@ -0,0 +1,92 @@ +config(['session' => false, 'db.table' => 'myusers']); + + $response = $auth->register([ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password', + ]); + + if (!$response) { + $this->fail(json_encode($auth->errors())); + } + + expect($response['user']['username'])->toBe('test-user'); +}); + +test('login should work with user defined table', function () { + $auth = auth(); + $auth->config(['session' => false, 'db.table' => 'myusers']); + + $response = $auth->login([ + 'username' => 'test-user', + 'password' => 'password' + ]); + + if (!$response) { + $this->fail(json_encode($auth->errors())); + } + + expect($response['user']['username'])->toBe('test-user'); +}); + +test('update should work with user defined table', function () { + $auth = auth(); + $auth->config(['session' => true, 'db.table' => 'myusers', 'session.lifetime' => '1 day']); + + $loginData = $auth->login([ + 'username' => 'test-user', + 'password' => 'password' + ]); + + if (!$loginData) { + $this->fail(json_encode($auth->errors())); + } + + $response = $auth->update([ + 'username' => 'test-user55', + 'email' => 'test-user55@example.com', + ]); + + if (!$response) { + $this->fail(json_encode($auth->errors())); + } + + expect($response['user']['username'])->toBe('test-user55'); + expect($response['user']['email'])->toBe('test-user55@example.com'); +}); + +test('user table can use uuid as id', function () { + createUsersTable('uuid_users', true); + + $auth = auth(); + $auth->config(['session' => false, 'db.table' => 'uuid_users']); + + $response = $auth->register([ + 'id' => '123e4567-e89b-12d3-a456-426614174000', + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password', + ]); + + if (!$response) { + $this->fail(json_encode($auth->errors())); + } + + expect($response['user']['username'])->toBe('test-user'); +}); From e49670c0eaf8ee3ebadda5a4afe8771fbe634474 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Mon, 21 Oct 2024 13:34:43 +0000 Subject: [PATCH 03/32] chore: generate actions --- .github/workflows/lint.yml | 34 ++++++++++++++++++++++++++++++++++ .github/workflows/tests.yml | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..ddd79f9 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,34 @@ +name: Lint code + +on: [push] + +jobs: + lint: + runs-on: ubuntu-latest + strategy: + fail-fast: true + + steps: + - name: Checkout code + uses: actions/checkout@v2 + with: + ref: ${{ github.head_ref }} + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + extensions: json, zip, dom, curl, libxml, mbstring + tools: composer:v2 + coverage: none + + - name: Install PHP dependencies + run: composer update --no-interaction --no-progress + + - name: Run Linter + run: composer run lint + + - name: Commit changes + uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: 'chore: fix styling' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..dbb61ca --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,32 @@ +name: Run Tests + +on: ["push","pull_request"] + +jobs: + tests: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + os: ["ubuntu-latest"] + php: ["8.3"] + + name: PHP ${{ matrix.php }} - ${{ matrix.os }} + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: json, zip, dom, curl, libxml, mbstring + tools: composer:v2 + coverage: xdebug + + - name: Install PHP dependencies + run: composer update --no-interaction --no-progress + + - name: Run Tests + run: composer run test From 25715dd9fbde516a04502b6338db5e3e85047429 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Mon, 21 Oct 2024 22:41:29 +0000 Subject: [PATCH 04/32] chore: update alchemy --- .github/workflows/tests.yml | 44 +++++++++++++++++++++--------------- alchemy.yml | 15 ++++++++++-- tests/table-actions.test.php | 1 + 3 files changed, 40 insertions(+), 20 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index dbb61ca..c8ca861 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,25 +8,33 @@ jobs: strategy: fail-fast: true matrix: - os: ["ubuntu-latest"] - php: ["8.3"] + os: ["ubuntu-latest","macos-latest","windows-latest"] + php: ["8.3","8.2","8.1","8.0","7.4"] name: PHP ${{ matrix.php }} - ${{ matrix.os }} steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - extensions: json, zip, dom, curl, libxml, mbstring - tools: composer:v2 - coverage: xdebug - - - name: Install PHP dependencies - run: composer update --no-interaction --no-progress - - - name: Run Tests - run: composer run test + - name: Checkout + uses: actions/checkout@v2 + + - name: Boot MySQL + run: sudo systemctl start mysql.service + + - name: Initialize database + run: | + mysql -e 'CREATE DATABASE atest;' \ + -uroot -p -P3306 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: json, zip, dom, curl, libxml, mbstring + tools: composer:v2 + coverage: xdebug + + - name: Install PHP dependencies + run: composer update --no-interaction --no-progress + + - name: Run Tests + run: composer run test -- --flags=coverage diff --git a/alchemy.yml b/alchemy.yml index 3e79ea7..754bb22 100644 --- a/alchemy.yml +++ b/alchemy.yml @@ -8,8 +8,13 @@ tests: - tests files: - '*.test.php' - coverage: - processUncoveredFiles: true + database: + type: mysql + connection: + name: atest + port: 3306 + username: root + password: '' lint: preset: PSR12 @@ -24,10 +29,16 @@ actions: - tests os: - ubuntu-latest + - macos-latest + - windows-latest php: extensions: json, zip, dom, curl, libxml, mbstring versions: - '8.3' + - '8.2' + - '8.1' + - '8.0' + - '7.4' events: - push - pull_request diff --git a/tests/table-actions.test.php b/tests/table-actions.test.php index a99c7f7..2055bd7 100644 --- a/tests/table-actions.test.php +++ b/tests/table-actions.test.php @@ -10,6 +10,7 @@ afterAll(function () { deleteUser('test-user', 'myusers'); deleteUser('test-user55', 'myusers'); + deleteUser('test-user', 'uuid_users'); }); test('register should save user in user defined table', function () { From 745b3259298c60565dd6fa2abf7e27f6689b4105 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Mon, 21 Oct 2024 22:44:41 +0000 Subject: [PATCH 05/32] chore: update test db credentials --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c8ca861..4a6fd3f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,7 +23,7 @@ jobs: - name: Initialize database run: | mysql -e 'CREATE DATABASE atest;' \ - -uroot -p -P3306 + -u root -P 3306 - name: Setup PHP uses: shivammathur/setup-php@v2 From 2599cbae93967e8170a76c5722173aa90e30d6d0 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Mon, 21 Oct 2024 22:45:57 +0000 Subject: [PATCH 06/32] chore: update test db credentials --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4a6fd3f..9cde493 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,7 +23,7 @@ jobs: - name: Initialize database run: | mysql -e 'CREATE DATABASE atest;' \ - -u root -P 3306 + -u root -p root -P 3306 - name: Setup PHP uses: shivammathur/setup-php@v2 From d48cb50495bf81ec2a4e4d931c47ccbb6ada9b1c Mon Sep 17 00:00:00 2001 From: mychidarko Date: Mon, 21 Oct 2024 22:48:51 +0000 Subject: [PATCH 07/32] chore: update test db credentials --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9cde493..d67758b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,7 +23,7 @@ jobs: - name: Initialize database run: | mysql -e 'CREATE DATABASE atest;' \ - -u root -p root -P 3306 + -uroot -proot - name: Setup PHP uses: shivammathur/setup-php@v2 From 60d314c33387aa1613b1f435ba9640a5f40e8f25 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Mon, 21 Oct 2024 22:52:43 +0000 Subject: [PATCH 08/32] chore: update test db credentials --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d67758b..73dd3a9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,7 +23,7 @@ jobs: - name: Initialize database run: | mysql -e 'CREATE DATABASE atest;' \ - -uroot -proot + -u root -p root - name: Setup PHP uses: shivammathur/setup-php@v2 From 7b55355f926a0b7d0e4ca74062c0213916a52dc1 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Mon, 21 Oct 2024 22:53:41 +0000 Subject: [PATCH 09/32] chore: update test db credentials --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 73dd3a9..e855f3d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,7 +23,7 @@ jobs: - name: Initialize database run: | mysql -e 'CREATE DATABASE atest;' \ - -u root -p root + -uroot -proot -P3306 - name: Setup PHP uses: shivammathur/setup-php@v2 From 9e8cf71271de0ce1cc21da713df8d40b8e613b2e Mon Sep 17 00:00:00 2001 From: mychidarko Date: Mon, 21 Oct 2024 23:02:28 +0000 Subject: [PATCH 10/32] chore: update alchemy file --- alchemy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alchemy.yml b/alchemy.yml index 754bb22..e18ba19 100644 --- a/alchemy.yml +++ b/alchemy.yml @@ -14,7 +14,7 @@ tests: name: atest port: 3306 username: root - password: '' + password: root lint: preset: PSR12 From b7ed64a838aca77a7fe5a34bb441418a8a6e2311 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Mon, 21 Oct 2024 23:04:34 +0000 Subject: [PATCH 11/32] test: update db connection --- tests/Pest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Pest.php b/tests/Pest.php index e3ca842..d6d0fb7 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -39,7 +39,7 @@ function getConnectionConfig(): array 'port' => '3306', 'host' => '127.0.0.1', 'username' => 'root', - 'password' => '', + 'password' => 'root', 'dbname' => 'atest', ]; } From 252c2875052d8a6ee6d5cde1b2be61f72870ede2 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Mon, 21 Oct 2024 23:07:20 +0000 Subject: [PATCH 12/32] test: update db connection --- tests/Pest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Pest.php b/tests/Pest.php index d6d0fb7..5367d75 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -37,7 +37,7 @@ function getConnectionConfig(): array { return [ 'port' => '3306', - 'host' => '127.0.0.1', + 'host' => 'localhost', 'username' => 'root', 'password' => 'root', 'dbname' => 'atest', From e23c7ac6c227e0eed9c8c0b350456fc802986f29 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Fri, 25 Oct 2024 01:29:46 +0000 Subject: [PATCH 13/32] feat: implement user layer + registration --- .github/workflows/lint.yml | 34 -- .github/workflows/tests.yml | 40 -- .gitignore | 3 +- alchemy.yml | 7 - composer.json | 3 +- src/Auth.php | 645 +++++++++++++-------------------- src/Auth/Config.php | 73 ++++ src/Auth/Core.php | 215 ----------- src/Auth/Session.php | 40 -- src/Auth/User.php | 132 +++++++ src/Auth/UsesRoles.php | 120 ++++++ src/Helpers/Authentication.php | 138 ------- src/Helpers/JWT.php | 371 ------------------- tests/Pest.php | 90 +---- tests/auth.test.php | 152 -------- tests/core.test.php | 7 + tests/extra.test.php | 27 -- tests/session.test.php | 183 ---------- tests/table-actions.test.php | 93 ----- tests/user.test.php | 14 + 20 files changed, 615 insertions(+), 1772 deletions(-) delete mode 100644 .github/workflows/lint.yml delete mode 100644 .github/workflows/tests.yml create mode 100644 src/Auth/Config.php delete mode 100644 src/Auth/Core.php delete mode 100644 src/Auth/Session.php create mode 100644 src/Auth/User.php create mode 100644 src/Auth/UsesRoles.php delete mode 100755 src/Helpers/Authentication.php delete mode 100755 src/Helpers/JWT.php delete mode 100644 tests/auth.test.php create mode 100644 tests/core.test.php delete mode 100644 tests/extra.test.php delete mode 100644 tests/session.test.php delete mode 100644 tests/table-actions.test.php create mode 100644 tests/user.test.php diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index ddd79f9..0000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Lint code - -on: [push] - -jobs: - lint: - runs-on: ubuntu-latest - strategy: - fail-fast: true - - steps: - - name: Checkout code - uses: actions/checkout@v2 - with: - ref: ${{ github.head_ref }} - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.2' - extensions: json, zip, dom, curl, libxml, mbstring - tools: composer:v2 - coverage: none - - - name: Install PHP dependencies - run: composer update --no-interaction --no-progress - - - name: Run Linter - run: composer run lint - - - name: Commit changes - uses: stefanzweifel/git-auto-commit-action@v4 - with: - commit_message: 'chore: fix styling' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index e855f3d..0000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Run Tests - -on: ["push","pull_request"] - -jobs: - tests: - runs-on: ${{ matrix.os }} - strategy: - fail-fast: true - matrix: - os: ["ubuntu-latest","macos-latest","windows-latest"] - php: ["8.3","8.2","8.1","8.0","7.4"] - - name: PHP ${{ matrix.php }} - ${{ matrix.os }} - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Boot MySQL - run: sudo systemctl start mysql.service - - - name: Initialize database - run: | - mysql -e 'CREATE DATABASE atest;' \ - -uroot -proot -P3306 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - extensions: json, zip, dom, curl, libxml, mbstring - tools: composer:v2 - coverage: xdebug - - - name: Install PHP dependencies - run: composer update --no-interaction --no-progress - - - name: Run Tests - run: composer run test -- --flags=coverage diff --git a/.gitignore b/.gitignore index 21cd976..536447e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,9 +5,8 @@ composer.lock package-lock.json vendor/ test/ +old/ *.tests.php -workflows -!.github/workflows # OS Generated .DS_Store* diff --git a/alchemy.yml b/alchemy.yml index e18ba19..aa85f3c 100644 --- a/alchemy.yml +++ b/alchemy.yml @@ -8,13 +8,6 @@ tests: - tests files: - '*.test.php' - database: - type: mysql - connection: - name: atest - port: 3306 - username: root - password: root lint: preset: PSR12 diff --git a/composer.json b/composer.json index 280c38d..6fe7bc9 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,8 @@ "leafs/db": "*", "leafs/form": "*", "leafs/http": "*", - "leafs/alchemy": "dev-next" + "leafs/alchemy": "dev-next", + "firebase/php-jwt": "^6.10" }, "config": { "allow-plugins": { diff --git a/src/Auth.php b/src/Auth.php index aafa745..01f4b46 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -2,544 +2,411 @@ namespace Leaf; -use Leaf\Auth\Core; -use Leaf\Helpers\Authentication; +use Firebase\JWT\JWT; +use Firebase\JWT\Key; +use Leaf\Auth\Config; +use Leaf\Auth\User; use Leaf\Helpers\Password; +use Leaf\Http\Session; /** * Leaf Simple Auth * ------------------------- - * Simple, straightforward authentication. + * Simple, lightweight authentication. * * @author Michael Darko * @since 1.5.0 - * @version 2.0.0 + * @version 3.0.0 */ -class Auth extends Core +class Auth { /** - * Simple user login - * - * @param array $credentials User credentials - * - * @return array|false false or all user info + tokens + session data + * The currently authenticated user + * @var User */ - public static function login(array $credentials) - { - static::leafDbConnect(); - - static::$errors = []; - $table = static::$settings['db.table']; - - if (static::config('session')) { - static::useSession(); - } - - $passKey = static::$settings['password.key']; - $password = $credentials[$passKey] ?? null; - - if (isset($credentials[$passKey])) { - unset($credentials[$passKey]); - } else { - static::$settings['password'] = false; - } - - $user = static::$db->select($table)->where($credentials)->fetchAssoc(); - - if (!$user) { - static::$errors['auth'] = static::$settings['messages.loginParamsError']; - return false; - } + protected $user; - if (static::$settings['password']) { - $passwordIsValid = (static::$settings['password.verify'] !== false && isset($user[$passKey])) - ? ((is_callable(static::$settings['password.verify'])) - ? call_user_func(static::$settings['password.verify'], $password, $user[$passKey]) - : Password::verify($password, $user[$passKey])) - : false; - - if (!$passwordIsValid) { - static::$errors['password'] = static::$settings['messages.loginPasswordError']; - return false; - } - } + /** + * Internal instance of Leaf DB + * @var Db + */ + protected $db; - $token = Authentication::generateSimpleToken( - $user[static::$settings['id.key']], - static::config('token.secret'), - static::config('token.lifetime') - ); + /** + * Internal instance of Leaf session + * @var Session + */ + protected $session; - if (isset($user[static::$settings['id.key']])) { - $userId = $user[static::$settings['id.key']]; + /** + * All errors caught + * @var array + */ + protected $errorsArray = []; - if (in_array(static::$settings['id.key'], static::$settings['hidden']) || in_array('field.id', static::$settings['hidden'])) { - unset($user[static::$settings['id.key']]); - } - } + /** + * Connect leaf auth to the database + * @param array $dbConfig Configuration for leaf db connection + * @return $this + */ + public function connect($dbConfig = []) + { + $this->db = new Db(); + $this->db->connect($dbConfig); - if ((in_array(static::$settings['password.key'], static::$settings['hidden']) || in_array('field.password', static::$settings['hidden'])) && (isset($user[$passKey]) || !$user[$passKey])) { - unset($user[$passKey]); + if (Config::get('session')) { + $this->initSession(Config::get('session.cookie')); } - if (!$token) { - static::$errors = array_merge(static::$errors, Authentication::errors()); - return false; - } + return $this; + } - if (static::config('session')) { - if (isset($userId)) { - $user[static::$settings['id.key']] = $userId; - } + /** + * Connect to database using environment variables + * + * @param array $pdoOptions Options for PDO connection + * @return $this + */ + public function autoConnect(array $pdoOptions = []) + { + $this->db = new Db(); + $this->db->autoConnect($pdoOptions); - self::setUserToSession($user, $token); + if (Config::get('session')) { + $this->initSession(Config::get('session.cookie')); } - $response['user'] = $user; - $response['token'] = $token; - - return $response; + return $this; } /** - * Simple user registration - * - * @param array $credentials Information for new user - * @param array $uniques Parameters which should be unique - * - * @return array|false false or all user info + tokens + session data + * Pass in db connection instance directly + * + * @param \PDO $connection A connection instance of your db + * @return $this; */ - public static function register(array $credentials) + public function dbConnection(\PDO $connection) { - static::leafDbConnect(); + $this->db = new Db(); + $this->db->connection($connection); - static::$errors = []; - $table = static::$settings['db.table']; - $passKey = static::$settings['password.key']; - - if (!isset($credentials[$passKey])) { - static::$settings['password'] = false; + if (Config::get('session')) { + $this->initSession(Config::get('session.cookie')); } - if (static::$settings['password'] && static::$settings['password.encode'] !== false) { - $credentials[$passKey] = (is_callable(static::$settings['password.encode'])) - ? call_user_func(static::$settings['password.encode'], $credentials[$passKey]) - : Password::hash($credentials[$passKey]); - - } + return $this; + } - if (static::$settings['timestamps']) { - $now = (new \Leaf\Date())->tick()->format(static::$settings['timestamps.format']); - $credentials['created_at'] = $now; - $credentials['updated_at'] = $now; - } + /** + * Sign a user in + * --- + * Verify user credentials and sign them in with token or session + * + * @param array $credentials User credentials + * @return bool + */ + public function login(array $credentials): bool + { + $this->checkDbConnection(); - if (isset($credentials[static::$settings['id.key']])) { - $credentials[static::$settings['id.key']] = is_callable($credentials[static::$settings['id.key']]) - ? call_user_func($credentials[static::$settings['id.key']]) - : $credentials[static::$settings['id.key']]; - } + $table = Config::get('db.table'); + $passwordKey = Config::get('password.key'); - try { - $query = static::$db->insert($table)->params($credentials)->unique(static::$settings['unique'])->execute(); - } catch (\Throwable $th) { - throw new \Exception($th->getMessage()); - } + $userPassword = $credentials[$passwordKey] ?? null; - if (!$query) { - static::$errors = array_merge(static::$errors, static::$db->errors()); - return false; + if ($userPassword) { + unset($credentials[$passwordKey]); } - $user = static::$db->select($table)->where($credentials)->fetchAssoc(); + $user = $this->db->select($table)->where($credentials)->first(); if (!$user) { - static::$errors = array_merge(static::$errors, static::$db->errors()); + $this->errorsArray['auth'] = Config::get('messages.loginParamsError'); return false; } - $token = Authentication::generateSimpleToken( - $user[static::$settings['id.key']], - static::config('token.secret'), - static::config('token.lifetime') - ); - - if (isset($user[static::$settings['id.key']])) { - $userId = $user[static::$settings['id.key']]; - } - - if ( - in_array(static::$settings['id.key'], static::$settings['hidden']) || in_array('field.id', static::$settings['hidden']) - ) { - unset($user[static::$settings['id.key']]); - } - - if ( - (in_array(static::$settings['password.key'], static::$settings['hidden']) || in_array('field.password', static::$settings['hidden'])) - && (isset($user[$passKey]) || !$user[$passKey]) - ) { - unset($user[$passKey]); - } + $passwordIsValid = (Config::get('password.verify') !== false && isset($user[$passwordKey])) + ? ((is_callable(Config::get('password.verify'))) + ? call_user_func(Config::get('password.verify'), $userPassword, $user[$passwordKey]) + : Password::verify($userPassword, $user[$passwordKey])) + : false; - if (!$token) { - static::$errors = array_merge(static::$errors, Authentication::errors()); + if (!$passwordIsValid) { + $this->errorsArray['password'] = Config::get('messages.loginPasswordError'); return false; } - if (static::config('session') && static::config('session.register')) { - static::useSession(); + echo json_encode($user); - if (isset($userId)) { - $user[static::$settings['id.key']] = $userId; - } - - self::setUserToSession($user, $token); - } - - $response['user'] = $user; - $response['token'] = $token; - - return $response; + return false; } /** - * Simple user update - * - * @param array $credentials New information for user - * @param array $uniques Parameters which should be unique - * - * @return array|false all user info + tokens + session data + * Register a new user + * --- + * Save a new user to the database + * + * @param array $userData User data + * @return bool */ - public static function update(array $credentials) + public function register(array $userData): bool { - static::leafDbConnect(); - - static::$errors = []; - - $table = static::$settings['db.table']; - - if (static::config('session')) { - static::useSession(); - } + $this->checkDbConnection(); - $passKey = static::$settings['password.key']; - $loggedInUser = static::user(); + $table = Config::get('db.table'); + $passwordKey = Config::get('password.key'); - if (!$loggedInUser) { - static::$errors['auth'] = 'Not authenticated'; - return false; - } - - $where = isset($loggedInUser[static::$settings['id.key']]) ? [static::$settings['id.key'] => $loggedInUser[static::$settings['id.key']]] : $loggedInUser; - - if (!isset($credentials[$passKey])) { - static::$settings['password'] = false; + if (Config::get('password.encode') !== false) { + $userData[$passwordKey] = (is_callable(Config::get('password.encode'))) + ? call_user_func(Config::get('password.encode'), $userData[$passwordKey]) + : Password::hash($userData[$passwordKey]); } - if (static::$settings['password'] && static::$settings['password.encode'] !== false) { - $credentials[$passKey] = (is_callable(static::$settings['password.encode'])) - ? call_user_func(static::$settings['password.encode'], $credentials[$passKey]) - : Password::hash($credentials[$passKey]); + if (Config::get('timestamps')) { + $now = (new Date())->tick()->format(Config::get('timestamps.format')); + $userData['created_at'] = $now; + $userData['updated_at'] = $now; } - if (static::$settings['timestamps']) { - $credentials['updated_at'] = (new \Leaf\Date())->tick()->format(static::$settings['timestamps.format']); + if (isset($credentials[Config::get('id.key')])) { + $userData[Config::get('id.key')] = is_callable($userData[Config::get('id.key')]) + ? call_user_func($userData[Config::get('id.key')]) + : $userData[Config::get('id.key')]; } - if (count(static::$settings['unique']) > 0) { - foreach (static::$settings['unique'] as $unique) { - if (!isset($credentials[$unique])) { - trigger_error("$unique not found in credentials."); - } - - $data = static::$db->select($table)->where($unique, $credentials[$unique])->fetchAssoc(); - - $wKeys = array_keys($where); - $wValues = array_values($where); - - if (isset($data[$wKeys[0]]) && $data[$wKeys[0]] != $wValues[0]) { - static::$errors[$unique] = "$unique already exists"; - } - } + try { + $query = $this->db->insert($table)->params($userData)->unique(Config::get('unique'))->execute(); - if (count(static::$errors) > 0) { + if (!$query) { + $this->errorsArray = array_merge($this->errorsArray, $this->db->errors()); return false; } - } - - try { - $query = static::$db->update($table)->params($credentials)->where($where)->execute(); } catch (\Throwable $th) { - trigger_error($th->getMessage()); - } - - if (!$query) { - static::$errors = array_merge(static::$errors, static::$db->errors()); - return false; - } - - if (isset($credentials['updated_at'])) { - unset($credentials['updated_at']); + throw new \Exception($th->getMessage()); } - $user = static::$db->select($table)->where($credentials)->fetchAssoc(); + $user = $this->db->select($table)->where($userData)->first(); if (!$user) { - static::$errors = array_merge(static::$errors, static::$db->errors()); - return false; - } - - $token = Authentication::generateSimpleToken( - $user[static::$settings['id.key']], - static::config('token.secret'), - static::config('token.lifetime') - ); - - if (isset($user[static::$settings['id.key']])) { - $userId = $user[static::$settings['id.key']]; - } - - if ( - (in_array(static::$settings['id.key'], static::$settings['hidden']) || in_array('field.id', static::$settings['hidden'])) - && isset($user[static::$settings['id.key']]) - ) { - unset($user[static::$settings['id.key']]); - } - - if ( - (in_array(static::$settings['password.key'], static::$settings['hidden']) || in_array('field.password', static::$settings['hidden'])) - && (isset($user[$passKey]) || !$user[$passKey]) - ) { - unset($user[$passKey]); - } - - if (!$token) { - static::$errors = array_merge(static::$errors, Authentication::errors()); + $this->errorsArray = array_merge($this->errorsArray, $this->db->errors()); return false; } - if (static::config('session')) { - if (isset($userId)) { - $user[static::$settings['id.key']] = $userId; - } - - static::$session->set('auth.user', $user); - static::$session->set('auth.token', $token); - } - - $response['user'] = $user; - $response['token'] = $token; + $this->user = new User($user); - return $response; + return true; } /** - * Manually start an auth session + * Update user data + * --- + * Update user data in the database + * + * @param array $userData User data + * @return bool */ - public static function useSession() + public function update(array $userData): bool { - static::config('session', true); - static::$session = Auth\Session::init(static::config('session.cookie')); + $this->checkDbConnection(); + return false; } /** - * Throw a 'use session' warning + * Get the id of the currently authenticated user + * @return string|int */ - protected static function sessionCheck() + public function id() { - if (!static::config('session')) { - trigger_error('Turn on sessions to use this feature.'); + if ($this->user) { + return $this->user->id(); } - if (!static::$session) { - static::useSession(); - } + return Config::get('session') + ? $this->getFromSession('auth.id') + : ($this->parseToken()['user.id'] ?? null); } /** - * Check session status + * Get the currently authenticated user + * @return User|null */ - public static function status() + public function user() { - static::sessionCheck(); - static::expireSession(); - - return static::$session->get('auth.token') ?? false; - } - - /** - * Return the user id encoded in token or session - */ - public static function id() - { - static::leafDbConnect(); - - static::$errors = []; + if ($this->user) { + return $this->user; + } - if (static::config('session')) { - if (static::expireSession()) { - return null; - } + $userId = $this->id(); - return static::$session->get('auth.token')[static::$settings['id.key']] ?? null; + if (!$userId) { + return null; } - $payload = static::validateToken(static::config('token.secret')); - - return $payload->user_id ?? null; - } + $idKey = Config::get('id.key'); + $table = Config::get('db.table'); - /** - * Get the current user data from token - * - * @param array $hidden Fields to hide from user array - */ - public static function user(array $hidden = []) - { - $table = static::$settings['db.table']; + $user = $this->db->select($table)->where($idKey, $userId)->first(); - if (!static::id()) { - return (static::config('session')) ? static::$session->get('auth.token') : null; + if (!$user) { + $this->errorsArray = $this->db->errors(); + return null; } - $user = static::$db->select($table)->where(static::$settings['id.key'], static::id())->fetchAssoc(); + $hidden = Config::get('hidden'); if (count($hidden) > 0) { foreach ($hidden as $item) { - if (isset($user[$item]) || !$user[$item]) { + if (isset($user[$item])) { unset($user[$item]); } } } - return $user; + return $this->user = new User( + $user + ); } /** - * End a session - * - * @param string $location A route to redirect to after logout + * Get data generated on user login + * @return object|null */ - public static function logout(?string $location = null) + public function data() { - static::sessionCheck(); - - static::$session->destroy(); + $user = $this->user(); - if (is_string($location)) { - \Leaf\Http\Headers::status(302); - $route = static::config($location) ?? $location; - - exit(header("location: $route")); + if (!$user) { + return null; } + + return $user->getAuthInfo(); } /** - * @return bool + * Parse the current user's token */ - private static function expireSession(): bool + public function parseToken() { - self::sessionCheck(); - - $sessionTtl = static::$session->get('session.ttl'); + $bearerToken = $this->getTokenFromRequest(); - if (!$sessionTtl) { - return false; + if ($bearerToken === null) { + return null; } - $isSessionExpired = time() > $sessionTtl; + return (array) JWT::decode( + $bearerToken, + new Key(Config::get('token.secret'), 'HS256') + ); + } - if ($isSessionExpired) { - static::$session->unset('auth.token'); - static::$session->unset('HAS_SESSION'); - static::$session->unset('auth.token'); - static::$session->unset('session.startedAt'); - static::$session->unset('session.lastActivity'); - static::$session->unset('session.ttl'); + protected function checkDbConnection(): void + { + if (!$this->db && function_exists('db')) { + if (db()->connection() instanceof \PDO || db()->autoConnect()) { + $this->db = db(); + } } - return $isSessionExpired; + if (!$this->db) { + throw new \Exception('You need to connect to your database first'); + } } - /** - * Session last active - */ - public static function lastActive() + protected function getFromSession($value) { - static::sessionCheck(); + if ($this->isSessionExpired()) { + return null; + } - return time() - static::$session->get('session.lastActivity'); + return Session::get($value); } - /** - * Refresh session - * - * @param bool $clearData Remove existing session data - */ - public static function refresh(bool $clearData = true) + protected function sessionCheck() { - static::sessionCheck(); + if (!Config::get('session')) { + throw new \Exception('Turn on sessions to use this feature.'); + } + } - $success = static::$session->regenerate($clearData); + protected function isSessionExpired(): bool + { + $sessionTtl = $this->session->get('session.ttl'); - static::$session->set('session.startedAt', time()); - static::$session->set('session.lastActivity', time()); - static::setSessionTtl(); + if (!$sessionTtl) { + return false; + } - return $success; - } + $isSessionExpired = time() > $sessionTtl; - /** - * Check how long a session has been going on - */ - public static function length() - { - static::sessionCheck(); + if ($isSessionExpired) { + $this->session->unset('auth.user'); + $this->session->unset('auth.id'); + $this->session->unset('auth.token'); + $this->session->unset('session.startedAt'); + $this->session->unset('session.lastActivity'); + $this->session->unset('session.ttl'); + } - return time() - static::$session->get('session.startedAt'); + return $isSessionExpired; } - /** - * @param array $user - * @param string $token - * - * @return void - */ - private static function setUserToSession(array $user, string $token): void + protected function initSession(array $sessionCookieParams = []) { - session_regenerate_id(); + $session = new Session(false); - static::$session->set('auth.token', $user); - static::$session->set('HAS_SESSION', true); - static::setSessionTtl(); + if (!isset($_SESSION)) { + session_set_cookie_params($sessionCookieParams); + session_start(); + } - if (static::config('SAVE_SESSION_JWT')) { - static::$session->set('auth.token', $token); + if (!$session->has('session.startedAt')) { + $session->set('session.startedAt', time()); } + + $session->set('session.lastActivity', time()); + + $this->session = $session; } - /** - * @return void - */ - private static function setSessionTtl(): void + protected function getTokenFromRequest() { - $sessionLifetime = static::config('session.lifetime'); - - if ($sessionLifetime === 0) { - return; + $headers = null; + + if (isset($_SERVER['Authorization'])) { + $headers = trim($_SERVER['Authorization']); + } elseif (isset($_SERVER['HTTP_AUTHORIZATION'])) { + $headers = trim($_SERVER['HTTP_AUTHORIZATION']); + } elseif (function_exists('apache_request_headers')) { + $requestHeaders = apache_request_headers(); + // Server-side fix for bug in old Android versions (a nice side-effect of this fix means we don't care about capitalization for Authorization) + $requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders)); + + if (isset($requestHeaders['Authorization'])) { + $headers = trim($requestHeaders['Authorization']); + } } - if (is_int($sessionLifetime)) { - static::$session->set('session.ttl', time() + $sessionLifetime); - return; + if (!empty($headers)) { + if (preg_match('/Bearer\s(\S+)/', $headers, $matches)) { + return $matches[1]; + } } - $sessionLifetimeInTime = strtotime($sessionLifetime); + $this->errorsArray['token'] = 'Access token not found'; - if (!$sessionLifetimeInTime) { - throw new \Exception('Provided string could not be converted to time'); - } + return null; + } - static::$session->set('session.ttl', $sessionLifetimeInTime); + protected function getTokenFromSession() + { + return \Leaf\Http\Session::get('auth.token'); + } + + /** + * Return all errors caught + */ + public function errors(): array + { + return $this->errorsArray; } } diff --git a/src/Auth/Config.php b/src/Auth/Config.php new file mode 100644 index 0000000..d7ef644 --- /dev/null +++ b/src/Auth/Config.php @@ -0,0 +1,73 @@ + 'id', + 'db.table' => 'users', + + 'timestamps' => true, + 'timestamps.format' => 'YYYY-MM-DD HH:mm:ss', + + 'password.encode' => null, + 'password.verify' => null, + 'password.key' => 'password', + + 'unique' => ['email', 'username'], + 'hidden' => ['field.id', 'field.password'], + + 'session' => false, + 'session.logout' => null, + 'session.register' => null, + 'session.lifetime' => 60 * 60 * 24, + 'session.cookie' => ['secure' => true, 'httponly' => true, 'samesite' => 'lax'], + + 'token.lifetime' => null, + 'token.secret' => '@_leaf$0Secret!', + + 'messages.loginParamsError' => 'Incorrect credentials!', + 'messages.loginPasswordError' => 'Password is incorrect!', + ]; + + /** + * Set Leaf Auth config + */ + public static function set($config) + { + static::$config = array_merge(static::$config, $config); + } + + /** + * Overwrite Leaf Auth config + */ + public static function overwrite($config) + { + static::$config = $config; + } + + /** + * Get Leaf Auth config + */ + public static function get($key = null) + { + if ($key) { + return static::$config[$key] ?? null; + } + + return static::$config; + } +} diff --git a/src/Auth/Core.php b/src/Auth/Core.php deleted file mode 100644 index 95fe009..0000000 --- a/src/Auth/Core.php +++ /dev/null @@ -1,215 +0,0 @@ - 'id', - 'db.table' => 'users', - - 'timestamps' => true, - 'timestamps.format' => 'c', - - 'password' => true, - 'password.encode' => null, - 'password.verify' => null, - 'password.key' => 'password', - - 'unique' => ['email', 'username'], - 'hidden' => ['field.id', 'field.password'], - - 'session' => false, - 'session.logout' => null, - 'session.register' => null, - 'session.lifetime' => 60 * 60 * 24, - 'session.cookie' => ['secure' => true, 'httponly' => true, 'samesite' => 'lax'], - - 'token.lifetime' => null, - 'token.secret' => '@_leaf$0Secret!', - - 'messages.loginParamsError' => 'Incorrect credentials!', - 'messages.loginPasswordError' => 'Password is incorrect!', - ]; - - /** - * @var \Leaf\Db - */ - protected static $db; - - /** - * @var \Leaf\Http\Session - */ - protected static $session; - - /** - * Connect leaf auth to the database - * - * @param string|array $host Host Name or full config - * @param string $dbname Database name - * @param string $user Database username - * @param string $password Database password - * @param string $dbtype Type of database: mysql, postgres, sqlite, ... - * @param array $pdoOptions Options for PDO connection - */ - public static function connect( - $host, - string $dbname = null, - string $user = null, - string $password = null, - string $dbtype = null, - array $pdoOptions = [] - ) { - $db = new \Leaf\Db(); - - if (is_array($host)) { - $db->connect($host); - } else { - $db->connect($host, $dbname, $user, $password, $dbtype, $pdoOptions); - } - - static::$db = $db; - } - - /** - * Connect to database using environment variables - * - * @param array $pdoOptions Options for PDO connection - */ - public static function autoConnect(array $pdoOptions = []) - { - $db = new \Leaf\Db(); - $db->autoConnect($pdoOptions); - - static::$db = $db; - } - - /** - * Pass in db connetion instance directly - * - * @param \PDO $connection A connection instance of your db - */ - public static function dbConnection(\PDO $connection) - { - $db = new \Leaf\Db(); - $db->connection($connection); - - static::$db = $db; - } - - /** - * Auto connect to leaf db - */ - protected static function leafDbConnect() - { - if (!static::$db && function_exists('db')) { - if (db()->connection() instanceof \PDO || db()->autoConnect()) { - static::$db = db(); - } - } - - if (!static::$db) { - trigger_error('You need to connect to your database first'); - } - } - - /** - * Set auth config - * - * @param string|array $config The auth config key or array of config - * @param mixed $value The value if $config is a string - */ - public static function config($config, $value = null) - { - if (is_array($config)) { - foreach ($config as $key => $configValue) { - static::config($key, $configValue); - } - } else { - if ($value === null) { - return static::$settings[$config] ?? null; - } - - static::$settings[$config] = $value; - } - } - - /** - * Validate Json Web Token - * - * @param string $token The token validate - * @param string $secretKey The secret key used to encode token - */ - public static function validateUserToken(string $token, ?string $secretKey = null) - { - $payload = Authentication::validate($token, $secretKey ?? static::config('token.secret')); - - if (!$payload) { - static::$errors = array_merge(static::$errors, Authentication::errors()); - return null; - } - - return $payload; - } - - /** - * Validate Bearer Token - * - * @param string $secretKey The secret key used to encode token - */ - public static function validateToken(?string $secretKey = null) - { - $payload = Authentication::validateToken($secretKey ?? static::config('token.secret')); - - if (!$payload) { - static::$errors = array_merge(static::$errors, Authentication::errors()); - return null; - } - - return $payload; - } - - /** - * Get Bearer token - */ - public static function getBearerToken() - { - $token = Authentication::getBearerToken(); - - if (!$token) { - static::$errors = array_merge(static::$errors, Authentication::errors()); - return null; - } - - return $token; - } - - /** - * Get all authentication errors as associative array - */ - public static function errors(): array - { - return static::$errors; - } -} diff --git a/src/Auth/Session.php b/src/Auth/Session.php deleted file mode 100644 index a80b735..0000000 --- a/src/Auth/Session.php +++ /dev/null @@ -1,40 +0,0 @@ -get('session.startedAt')) { - static::$session->set('session.startedAt', time()); - } - - static::$session->set('session.lastActivity', time()); - - return static::$session; - } -} diff --git a/src/Auth/User.php b/src/Auth/User.php new file mode 100644 index 0000000..45d5aed --- /dev/null +++ b/src/Auth/User.php @@ -0,0 +1,132 @@ +data = $data; + + $this->tokens['access'] = $this->generateToken($accessTokenLifetime); + $this->tokens['refresh'] = $this->generateToken($accessTokenLifetime + 259200); + } + + /** + * Return the id of current user + * @return string|int + */ + public function id() + { + return $this->data['id'] ?? null; + } + + /** + * Get auth information to be sent to the client + * @return object + */ + public function getAuthInfo(): object + { + $dataToReturn = (object) [ + 'user' => $this->data, + 'accessToken' => $this->tokens['access'], + 'refreshToken' => $this->tokens['refresh'], + ]; + + if (count($this->roles)) { + $dataToReturn->roles = $this->roles; + } + + if (count($this->permissions)) { + $dataToReturn->permissions = $this->permissions; + } + + return $dataToReturn; + } + + /** + * Generate a new JWT for the user + * @return string + */ + public function generateToken($tokenLifetime): string + { + $userIdKey = Config::get('id.key'); + $secretPhrase = Config::get('token.secret'); + + // no fallback because we need the user id + $userId = $this->data[$userIdKey]; + + $payload = [ + 'user.id' => $userId, + 'iat' => time(), + 'exp' => $tokenLifetime, + 'iss' => $_SERVER['HTTP_HOST'] ?? 'localhost', + ]; + + $token = JWT::encode($payload, $secretPhrase, 'HS256'); + + return $token; + } + + public function get() + { + return $this->data; + } + + public function __toString() + { + return json_encode($this->data); + } + + public function __get($name) + { + return $this->data[$name] ?? null; + } + + public function __set($name, $value) + { + $this->data[$name] = $value; + } + + public function __isset($name) + { + return isset($this->data[$name]); + } + + public function __unset($name) + { + unset($this->data[$name]); + } +} diff --git a/src/Auth/UsesRoles.php b/src/Auth/UsesRoles.php new file mode 100644 index 0000000..8ff8099 --- /dev/null +++ b/src/Auth/UsesRoles.php @@ -0,0 +1,120 @@ +permissions = array_merge( + $this->permissions, + is_array($permission) ? $permission : [$permission] + ); + } + + /** + * Assign new role to user + * @param string|array $role The role to assign + */ + public function assignRoles($role): void + { + // will need to verify roles here + + $this->roles = array_merge( + $this->roles, + is_array($role) ? $role : [$role] + ); + + // persist via storage contract + + foreach ($this->roles as $role) { + $this->grantPermissions($this->getRolePermissions($role)); + } + + // persist via storage contract + } + + /** + * Check if user has a permission + * @param string|array $permission The permission(s) to check + * @return bool + */ + public function can($permission): bool + { + if (is_array($permission)) { + return count(array_intersect($permission, $this->permissions)) === count($permission); + } + + return in_array($permission, $this->permissions); + } + + /** + * Check if user has a role + * @param string|array $role The role(s) to check + * @return bool + */ + public function is($role): bool + { + if (is_array($role)) { + return count(array_intersect($role, $this->roles)) === count($role); + } + + return in_array($role, $this->roles); + } + + /** + * Revoke a permission from a user + * @param string|array $permission The permission(s) to revoke + */ + public function revokePermissions($permission): void + { + // persist via storage contract + $this->permissions = array_diff( + $this->permissions, + is_array($permission) ? $permission : [$permission] + ); + } + + /** + * Remove a role from a user + * @param string|array $role The role(s) to revoke + */ + public function removeRoles($role): void + { + // persist via storage contract + $this->roles = array_diff( + $this->roles, + is_array($role) ? $role : [$role] + ); + } + + /** + * Get the permissions for a role + * @param string $role + * @return array + */ + protected function getRolePermissions($role): array + { + // get permissions from storage contract + return []; + } +} diff --git a/src/Helpers/Authentication.php b/src/Helpers/Authentication.php deleted file mode 100755 index 6cd01f4..0000000 --- a/src/Helpers/Authentication.php +++ /dev/null @@ -1,138 +0,0 @@ - - * @since v1.2.0 - */ -class Authentication -{ - /** - * Any errors caught - */ - protected static $errorsArray = []; - - /** - * Quickly generate a JWT encoding a user id - * - * @param string $userId The user id to encode - * @param string $secretPhrase The user id to encode - * @param int $expiresAt Token lifetime - * - * @return string The generated token - */ - public static function generateSimpleToken(string $userId, string $secretPhrase, int $expiresAt = null): string - { - $payload = [ - 'iat' => time(), - 'iss' => 'localhost', - 'exp' => time() + ($expiresAt ?? (60 * 60 * 24)), - 'user_id' => $userId - ]; - - return self::generateToken($payload, $secretPhrase); - } - - /** - * Create a JWT with your own payload - * - * @param array $payload The JWT payload - * @param string $secretPhrase The user id to encode - * - * @return string The generated token - */ - public static function generateToken(array $payload, string $secretPhrase): string - { - return JWT::encode($payload, $secretPhrase); - } - - /** - * Get Authorization Headers - */ - public static function getAuthorizationHeader() - { - $headers = null; - - if (isset($_SERVER['Authorization'])) { - $headers = trim($_SERVER['Authorization']); - } elseif (isset($_SERVER['HTTP_AUTHORIZATION'])) { - $headers = trim($_SERVER['HTTP_AUTHORIZATION']); - } elseif (function_exists('apache_request_headers')) { - $requestHeaders = apache_request_headers(); - // Server-side fix for bug in old Android versions (a nice side-effect of this fix means we don't care about capitalization for Authorization) - $requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders)); - - if (isset($requestHeaders['Authorization'])) { - $headers = trim($requestHeaders['Authorization']); - } - } - - return $headers; - } - - /** - * get access token from header - */ - public static function getBearerToken() - { - $headers = self::getAuthorizationHeader(); - - if (!empty($headers)) { - if (preg_match('/Bearer\s(\S+)/', $headers, $matches)) { - return $matches[1]; - } - - self::$errorsArray['token'] = 'Access token not found'; - return null; - } - - self::$errorsArray['token'] = 'Access token not found'; - return null; - } - - /** - * Validate and decode access token in header - */ - public static function validateToken($secretPhrase) - { - $bearerToken = self::getBearerToken(); - if ($bearerToken === null) { - return null; - } - - return self::validate($bearerToken, $secretPhrase); - } - - /** - * Validate access token - * - * @param string $token Access token to validate and decode - */ - public static function validate($token, $secretPhrase) - { - try { - $payload = JWT::decode($token, $secretPhrase, ['HS256']); - if ($payload !== null) { - return $payload; - } - } catch (\DomainException $exception) { - self::$errorsArray['token'] = 'Malformed token'; - } - - self::$errorsArray = array_merge(self::$errorsArray, JWT::errors()); - return null; - } - - /** - * Get all authentication errors as associative array - */ - public static function errors() - { - return self::$errorsArray; - } -} diff --git a/src/Helpers/JWT.php b/src/Helpers/JWT.php deleted file mode 100755 index fe48a98..0000000 --- a/src/Helpers/JWT.php +++ /dev/null @@ -1,371 +0,0 @@ - - * @author Anant Narayanan - * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD - * @link https://github.com/firebase/php-jwt - */ -class JWT -{ - /** - * Errors caught - */ - protected static $errorsArray = []; - /** - * When checking nbf, iat or expiration times, - * we want to provide some extra leeway time to - * account for clock skew. - */ - public static $leeway = 0; - /** - * Allow the current timestamp to be specified. - * Useful for fixing a value within unit testing. - * - * Will default to PHP time() value if null. - */ - public static $timestamp = null; - public static $supported_algs = array( - 'HS256' => array('hash_hmac', 'SHA256'), - 'HS512' => array('hash_hmac', 'SHA512'), - 'HS384' => array('hash_hmac', 'SHA384'), - 'RS256' => array('openssl', 'SHA256'), - 'RS384' => array('openssl', 'SHA384'), - 'RS512' => array('openssl', 'SHA512'), - ); - /** - * Decodes a JWT string into a PHP object. - * - * @param string $jwt The JWT - * @param string|array $key The key, or map of keys. - * If the algorithm used is asymmetric, this is the public key - * @param array $allowed_algs List of supported verification algorithms - * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' - * - * @return object The JWT's payload as a PHP object - * - * - * @uses jsonDecode - * @uses urlsafeB64Decode - */ - public static function decode($jwt, $key, array $allowed_algs = array()) - { - $timestamp = is_null(static::$timestamp) ? time() : static::$timestamp; - if (empty($key)) { - return static::saveErr('Key may not be empty'); - } - $tks = explode('.', $jwt); - if (count($tks) != 3) { - return static::saveErr('Wrong number of segments'); - } - list($headb64, $bodyb64, $cryptob64) = $tks; - if (null === ($header = static::jsonDecode(static::urlsafeB64Decode($headb64)))) { - return static::saveErr('Invalid header encoding'); - } - if (null === $payload = static::jsonDecode(static::urlsafeB64Decode($bodyb64))) { - return static::saveErr('Invalid claims encoding'); - } - if (false === ($sig = static::urlsafeB64Decode($cryptob64))) { - return static::saveErr('Invalid signature encoding'); - } - if (empty($header->alg)) { - return static::saveErr('Empty algorithm'); - } - if (empty(static::$supported_algs[$header->alg])) { - return static::saveErr('Algorithm not supported'); - } - if (!in_array($header->alg, $allowed_algs)) { - return static::saveErr('Algorithm not allowed'); - } - if (is_array($key) || $key instanceof \ArrayAccess) { - if (isset($header->kid)) { - if (!isset($key[$header->kid])) { - return static::saveErr("'kid' invalid, unable to lookup correct key"); - } - $key = $key[$header->kid]; - } else { - return static::saveErr("'kid' empty, unable to lookup correct key"); - } - } - // Check the signature - if (!static::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) { - return static::saveErr('Signature verification failed'); - } - // Check if the nbf if it is defined. This is the time that the - // token can actually be used. If it's not yet that time, abort. - if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) { - return static::saveErr( - 'Cannot handle token prior to ' . date(\DateTime::ISO8601, $payload->nbf) - ); - } - // Check that this token has been created before "now". This prevents - // using tokens that have been created for later use (and haven't - // correctly used the nbf claim). - if (isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) { - return static::saveErr( - 'Cannot handle token prior to ' . date(\DateTime::ISO8601, $payload->iat) - ); - } - // Check if this token has expired. - if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) { - return static::saveErr('Expired token'); - } - return $payload; - } - - /** - * Converts and signs a PHP object or array into a JWT string. - * - * @param object|array $payload PHP object or array - * @param string $key The secret key. - * If the algorithm used is asymmetric, this is the private key - * @param string $alg The signing algorithm. - * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' - * @param mixed $keyId - * @param array $head An array with header elements to attach - * - * @return string A signed JWT - * - * @uses jsonEncode - * @uses urlsafeB64Encode - */ - public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $head = null) - { - $header = array('typ' => 'JWT', 'alg' => $alg); - if ($keyId !== null) { - $header['kid'] = $keyId; - } - if (isset($head) && is_array($head)) { - $header = array_merge($head, $header); - } - $segments = array(); - $segments[] = static::urlsafeB64Encode(static::jsonEncode($header)); - $segments[] = static::urlsafeB64Encode(static::jsonEncode($payload)); - $signing_input = implode('.', $segments); - $signature = static::sign($signing_input, $key, $alg); - $segments[] = static::urlsafeB64Encode($signature); - return implode('.', $segments); - } - - /** - * Sign a string with a given key and algorithm. - * - * @param string $msg The message to sign - * @param string|resource $key The secret key - * @param string $alg The signing algorithm. - * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' - * - * @return string An encrypted message - * - * @throws \DomainException Unsupported algorithm was specified - */ - public static function sign($msg, $key, $alg = 'HS256') - { - if (empty(static::$supported_algs[$alg])) { - throw new \DomainException('Algorithm not supported'); - } - list($function, $algorithm) = static::$supported_algs[$alg]; - switch ($function) { - case 'hash_hmac': - return hash_hmac($algorithm, $msg, $key, true); - case 'openssl': - $signature = ''; - $success = openssl_sign($msg, $signature, $key, $algorithm); - if (!$success) { - throw new \DomainException('OpenSSL unable to sign data'); - } else { - return $signature; - } - } - } - - /** - * Verify a signature with the message, key and method. Not all methods - * are symmetric, so we must have a separate verify and sign method. - * - * @param string $msg The original message (header and body) - * @param string $signature The original signature - * @param string|resource $key For HS*, a string key works. for RS*, must be a resource of an openssl public key - * @param string $alg The algorithm - * - * @return bool - * - * @throws \DomainException Invalid Algorithm or OpenSSL failure - */ - private static function verify($msg, $signature, $key, $alg) - { - if (empty(static::$supported_algs[$alg])) { - throw new \DomainException('Algorithm not supported'); - } - list($function, $algorithm) = static::$supported_algs[$alg]; - switch ($function) { - case 'openssl': - $success = openssl_verify($msg, $signature, $key, $algorithm); - if ($success === 1) { - return true; - } elseif ($success === 0) { - return false; - } - // returns 1 on success, 0 on failure, -1 on error. - throw new \DomainException( - 'OpenSSL error: ' . openssl_error_string() - ); - case 'hash_hmac': - default: - $hash = hash_hmac($algorithm, $msg, $key, true); - if (function_exists('hash_equals')) { - return hash_equals($signature, $hash); - } - $len = min(static::safeStrlen($signature), static::safeStrlen($hash)); - $status = 0; - for ($i = 0; $i < $len; $i++) { - $status |= (ord($signature[$i]) ^ ord($hash[$i])); - } - $status |= (static::safeStrlen($signature) ^ static::safeStrlen($hash)); - return ($status === 0); - } - } - - /** - * Decode a JSON string into a PHP object. - * - * @param string $input JSON string - * - * @return object Object representation of JSON string - * - * @throws \DomainException Provided string was invalid JSON - */ - public static function jsonDecode($input) - { - if (version_compare(PHP_VERSION, '5.4.0', '>=') && !(defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) { - /** In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you - * to specify that large ints (like Steam Transaction IDs) should be treated as - * strings, rather than the PHP default behaviour of converting them to floats. - */ - $obj = json_decode($input, false, 512, JSON_BIGINT_AS_STRING); - } else { - /** Not all servers will support that, however, so for older versions we must - * manually detect large ints in the JSON string and quote them (thus converting - *them to strings) before decoding, hence the preg_replace() call. - */ - $max_int_length = strlen((string) PHP_INT_MAX) - 1; - $json_without_bigints = preg_replace('/:\s*(-?\d{' . $max_int_length . ',})/', ': "$1"', $input); - $obj = json_decode($json_without_bigints); - } - if (function_exists('json_last_error') && $errno = json_last_error()) { - static::handleJsonError($errno); - } elseif ($obj === null && $input !== 'null') { - throw new \DomainException('Null result with non-null input'); - } - return $obj; - } - - /** - * Encode a PHP object into a JSON string. - * - * @param object|array $input A PHP object or array - * - * @return string JSON representation of the PHP object or array - * - * @throws \DomainException Provided object could not be encoded to valid JSON - */ - public static function jsonEncode($input) - { - $json = json_encode($input); - if (function_exists('json_last_error') && $errno = json_last_error()) { - static::handleJsonError($errno); - } elseif ($json === 'null' && $input !== null) { - throw new \DomainException('Null result with non-null input'); - } - return $json; - } - - /** - * Decode a string with URL-safe Base64. - * - * @param string $input A Base64 encoded string - * - * @return string A decoded string - */ - public static function urlsafeB64Decode($input) - { - $remainder = strlen($input) % 4; - if ($remainder) { - $padlen = 4 - $remainder; - $input .= str_repeat('=', $padlen); - } - return base64_decode(strtr($input, '-_', '+/')); - } - - /** - * Encode a string with URL-safe Base64. - * - * @param string $input The string you want encoded - * - * @return string The base64 encode of what you passed in - */ - public static function urlsafeB64Encode($input) - { - return str_replace('=', '', strtr(base64_encode($input), '+/', '-_')); - } - - /** - * Helper method to create a JSON error. - * - * @param int $errno An error number from json_last_error() - * - * @return void - */ - private static function handleJsonError($errno) - { - $messages = array( - JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', - JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON', - JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', - JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON', - JSON_ERROR_UTF8 => 'Malformed UTF-8 characters' //PHP >= 5.3.3 - ); - throw new \DomainException( - isset($messages[$errno]) - ? $messages[$errno] - : 'Unknown JSON error: ' . $errno - ); - } - - /** - * Get the number of bytes in cryptographic strings. - * - * @param string - * - * @return int - */ - private static function safeStrlen($str) - { - if (function_exists('mb_strlen')) { - return mb_strlen($str, '8bit'); - } - return strlen($str); - } - - protected static function saveErr($err, $key = 'token') - { - self::$errorsArray[$key] = $err; - return null; - } - - /** - * Return all errors found - */ - public static function errors() - { - return static::$errorsArray; - } -} diff --git a/tests/Pest.php b/tests/Pest.php index 5367d75..0688b0b 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,91 +1,21 @@ connect(getConnectionConfig()); - - // $auth = new \Leaf\Auth(); - // $auth->dbConnection($db->connection()); - - $db->createTableIfNotExists( - $table, - [ - // using varchar(255) to mimic binary(16) for uuid - 'id' => $dynamicId ? 'varchar(255)' : 'int NOT NULL AUTO_INCREMENT', - 'username' => 'varchar(255)', - 'email' => 'varchar(255)', - 'password' => 'varchar(255)', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', - 'PRIMARY KEY' => '(id)', - ] - )->execute(); - - $db->close(); -} - -function deleteUser(string $username, $table = 'users') -{ - $db = new \Leaf\Db(); - $db->connect(getConnectionConfig()); - - $db->delete($table)->where('username', $username)->execute(); -} - -function getConnectionConfig(): array +function getDatabaseConnection(): array { return [ - 'port' => '3306', - 'host' => 'localhost', - 'username' => 'root', - 'password' => 'root', - 'dbname' => 'atest', + 'dbtype' => 'pgsql', + 'port' => '5432', + 'host' => 'ep-autumn-block-a28alwsy.eu-central-1.aws.neon.tech', + 'username' => 'sandbox_owner', + 'password' => 'WH1qpBIf7LYc', + 'dbname' => 'sandbox', ]; } -function auth(): \Leaf\Auth +function connectToDatabase(): \Leaf\Db { $db = new \Leaf\Db(); - $db->connect(getConnectionConfig()); - - $auth = new \Leaf\Auth(); - $auth->dbConnection($db->connection()); - - return $auth; -} - -function getAuthConfig(array $settingsReplacement = []): array -{ - $settings = [ - 'id.key' => 'id', - 'id.uuid' => null, - - 'db.table' => 'users', - - 'timestamps' => true, - 'timestamps.format' => 'c', - - 'password' => true, - 'password.encode' => null, - 'password.verify' => null, - 'password.key' => 'password', - - 'unique' => ['email', 'username'], - 'hidden' => ['field.id', 'field.password'], - - 'session' => false, - 'session.logout' => null, - 'session.register' => null, - 'session.lifetime' => 60 * 60 * 24, - 'session.cookie' => ['secure' => true, 'httponly' => true, 'samesite' => 'lax'], - - 'token.lifetime' => null, - 'token.secret' => '@_leaf$0Secret!', - - 'messages.loginParamsError' => 'Incorrect credentials!', - 'messages.loginPasswordError' => 'Password is incorrect!', - ]; + $db->connect(getDatabaseConnection()); - return array_replace($settings, $settingsReplacement); + return $db; } diff --git a/tests/auth.test.php b/tests/auth.test.php deleted file mode 100644 index 0ccfcad..0000000 --- a/tests/auth.test.php +++ /dev/null @@ -1,152 +0,0 @@ -config(['timestamps.format' => 'YYYY-MM-DD HH:MM:ss']); - - $response = $auth->register([ - 'username' => 'test-user', - 'email' => 'test-user@example.com', - 'password' => 'password', - ]); - - if (!$response) { - $this->fail(json_encode($auth->errors())); - } - - expect($response['user']['username'])->toBe('test-user'); -}); - -test('register should fail if user already exists', function () { - $auth = auth(); - - $auth->config([ - 'timestamps.format' => 'YYYY-MM-DD HH:MM:ss', - 'unique' => ['username', 'email'] - ]); - - $response = $auth->register([ - 'username' => 'test-user', - 'email' => 'test-user@example.com', - 'password' => 'password' - ]); - - expect($response)->toBe(false); - expect($auth->errors()['email'])->toBe('email already exists'); - expect($auth->errors()['username'])->toBe('username already exists'); -}); - -test('login should retrieve user from database', function () { - $auth = auth(); - - $response = $auth->login([ - 'username' => 'test-user', - 'password' => 'password' - ]); - - if (!$response) { - $this->fail(json_encode($auth->errors())); - } - - expect($response['user']['username'])->toBe('test-user'); -}); - -test('login should fail if user does not exist', function () { - $auth = auth(); - - $response = $auth->login([ - 'username' => 'non-existent-user', - 'password' => 'password' - ]); - - expect($response)->toBe(false); - expect($auth->errors()['auth'])->toBe('Incorrect credentials!'); -}); - -test('login should fail if password is wrong', function () { - $db = new \Leaf\Db(); - $db->connect(getConnectionConfig()); - - $db - ->insert('users') - ->params([ - 'username' => 'test-user', - 'email' => 'test-user@example.com', - 'password' => '$2y$10$91T2Y5/D4e9QXw8EgU33E.9J1N23hHg.6lG5ofVhh69la492kqKga', - ]) - ->execute(); - - $auth = new \Leaf\Auth(); - $auth->dbConnection($db->connection()); - - $userData = $auth->login([ - 'username' => 'test-user', - 'password' => 'wrong-password' - ]); - - expect($userData)->toBe(false); - expect($auth->errors()['password'])->toBe('Password is incorrect!'); -}); - -test('update should update user in database', function () { - $auth = auth(); - - $auth->useSession(); - - $data = $auth->login([ - 'username' => 'test-user', - 'password' => 'password' - ]); - - if (!$data) { - $this->fail(json_encode($auth->errors())); - } - - $response = $auth->update([ - 'username' => 'test-user22', - 'email' => 'test-user22@test.com', - ]); - - if (!$response) { - $this->fail(json_encode($auth->errors())); - } - - $auth->config(['session' => false]); - - expect($response['user']['username'])->toBe('test-user22'); - expect($response['user']['email'])->toBe('test-user22@test.com'); -}); - -test('update should fail if user already exists', function () { - $auth = auth(); - - $auth->useSession(); - - $data = $auth->login([ - 'username' => 'test-user22', - 'password' => 'password' - ]); - - if (!$data) { - $this->fail(json_encode($auth->errors())); - } - - $response = $auth->update([ - 'username' => 'test-user', - 'email' => 'test-user@example.com', - ]); - - expect($response)->toBe(false); - expect($auth->errors()['email'])->toBe('email already exists'); - expect($auth->errors()['username'])->toBe('username already exists'); -}); diff --git a/tests/core.test.php b/tests/core.test.php new file mode 100644 index 0000000..8a7d9ad --- /dev/null +++ b/tests/core.test.php @@ -0,0 +1,7 @@ +toBeInstanceOf(\Leaf\Db::class); +}); diff --git a/tests/extra.test.php b/tests/extra.test.php deleted file mode 100644 index ff33dca..0000000 --- a/tests/extra.test.php +++ /dev/null @@ -1,27 +0,0 @@ -config(['timestamps.format' => 'YYYY-MM-DD HH:MM:ss']); - - $auth->register([ - 'username' => 'extra-user', - 'email' => 'extra-user@example.com', - 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', - ]); -}); - -afterAll(function () { - deleteUser('extra-user'); -}); - -test('login should produce valid token', function () { - $auth = auth(); - - $auth->config(['hidden' => ['password']]); - $data = $auth->login(['username' => 'extra-user', 'password' => 'login-pass']); - - expect($auth->validateUserToken($data['token'])->user_id)->toBe((string) $data['user']['id']); -}); diff --git a/tests/session.test.php b/tests/session.test.php deleted file mode 100644 index cd331f6..0000000 --- a/tests/session.test.php +++ /dev/null @@ -1,183 +0,0 @@ -config(['timestamps.format' => 'YYYY-MM-DD HH:MM:ss']); - - $auth->register([ - 'username' => 'login-user', - 'email' => 'login-user@example.com', - 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', - ]); -}); - -afterEach(function () { - if (!session_status()) { - session_start(); - } - - session_destroy(); -}); - -afterAll(function () { - deleteUser('login-user'); -}); - -test('login should set user session', function () { - $auth = auth(); - $auth->useSession(); - - $auth->login(['username' => 'login-user', 'password' => 'login-pass']); - - $session = new \Leaf\Http\Session(false); - $user = $session->get('auth.token'); - - expect($user['username'])->toBe('login-user'); -}); - -test('login should set session ttl', function () { - $auth = auth(); - $auth->useSession(); - - $timeBeforeLogin = time(); - $auth->login(['username' => 'login-user', 'password' => 'login-pass']); - - $session = new \Leaf\Http\Session(false); - $sessionTtl = $session->get('session.ttl'); - - expect($sessionTtl > $timeBeforeLogin)->toBeTrue(); -}); - -test('login should set regenerate session id', function () { - $auth = auth(); - $auth->useSession(); - - $auth->login(['username' => 'login-user', 'password' => 'login-pass']); - - $originalSessionId = session_id(); - - $auth->login(['username' => 'login-user', 'password' => 'login-pass']); - - expect(session_id())->not()->toBe($originalSessionId); -}); - -test('login should set secure session cookie params', function () { - $auth = auth(); - $auth->useSession(); - - $auth->login(['username' => 'login-user', 'password' => 'login-pass']); - - $cookieParams = session_get_cookie_params(); - - expect($cookieParams['secure'])->toBeTrue(); - expect($cookieParams['httponly'])->toBeTrue(); - expect($cookieParams['samesite'])->toBe('lax'); -}); - -test('Session should expire when fetching user, and then login is possible again', function () { - $auth = new \Leaf\Auth(); - $auth->config(['session.lifetime' => 2]); - - $auth->login(['username' => 'login-user', 'password' => 'login-pass']); - - $user = $auth->user(); - - expect($user)->not()->toBeNull(); - expect($user['username'])->toBe('login-user'); - - sleep(1); - expect($auth->user())->not()->toBeNull(); - - sleep(2); - expect($auth->user())->toBeNull(); - - $userAfterReLogin = $auth->login(['username' => 'login-user', 'password' => 'login-pass']); - - expect($userAfterReLogin)->not()->toBeNull(); - expect($userAfterReLogin['user']['username'])->toBe('login-user'); -}); - -test('Session should not expire when fetching user if session lifetime is 0', function () { - $auth = new \Leaf\Auth(); - $auth->config(['session.lifetime' => 0]); - - $auth->login(['username' => 'login-user', 'password' => 'login-pass']); - - $user = $auth->user(); - - expect($user)->not()->toBeNull(); - expect($user['username'])->toBe('login-user'); - - sleep(2); - expect($auth->user())->not()->toBeNull(); -}); - -test('Session should expire when fetching user id', function () { - $auth = new \Leaf\Auth(); - - $auth->config(['session.lifetime' => 2]); - $auth->login(['username' => 'login-user', 'password' => 'login-pass']); - - expect($auth->id())->not()->toBeNull(); - - sleep(1); - expect($auth->id())->not()->toBeNull(); - - sleep(2); - expect($auth->id())->toBeNull(); -}); - -test('Session should expire when fetching status', function () { - $auth = new \Leaf\Auth(); - - $auth->config(['session.lifetime' => 2]); - $auth->login(['username' => 'login-user', 'password' => 'login-pass']); - - expect($auth->status())->not()->toBeNull(); - - sleep(1); - expect($auth->status())->not()->toBeNull(); - - sleep(2); - expect($auth->status())->toBeFalse(); -}); - -test('Session lifetime should set correct session ttl when string is configured instead of timestamp', function () { - $auth = new \Leaf\Auth(); - - $auth->config(['session.lifetime' => '1 day']); - $auth->login(['username' => 'login-user', 'password' => 'login-pass']); - - expect($auth->status())->not()->toBeNull(); - - $timestampOneDay = 60 * 60 * 24; - $session = new \Leaf\Http\Session(false); - $sessionTtl = $session->get('session.ttl'); - - expect($sessionTtl)->toBe(time() + $timestampOneDay); -}); - -test('Login should throw error when lifetime string is invalid', function () { - $auth = new \Leaf\Auth(); - $auth->config(['session.lifetime' => 'invalid string']); - - expect(function () use ($auth) { - return $auth->login(['username' => 'login-user', 'password' => 'login-pass']); - })->toThrow(Exception::class, 'Provided string could not be converted to time'); -}); - -test('Login should set session ttl on login', function () { - $auth = auth(); - $auth->useSession(); - $auth->config(['session.lifetime' => 2]); - - $timeBeforeLogin = time(); - $auth->login(['username' => 'login-user', 'password' => 'login-pass']); - - $session = new \Leaf\Http\Session(false); - $sessionTtl = $session->get('session.ttl'); - - expect($sessionTtl > $timeBeforeLogin)->toBeTrue(); -}); diff --git a/tests/table-actions.test.php b/tests/table-actions.test.php deleted file mode 100644 index 2055bd7..0000000 --- a/tests/table-actions.test.php +++ /dev/null @@ -1,93 +0,0 @@ -config(['session' => false, 'db.table' => 'myusers']); - - $response = $auth->register([ - 'username' => 'test-user', - 'email' => 'test-user@example.com', - 'password' => 'password', - ]); - - if (!$response) { - $this->fail(json_encode($auth->errors())); - } - - expect($response['user']['username'])->toBe('test-user'); -}); - -test('login should work with user defined table', function () { - $auth = auth(); - $auth->config(['session' => false, 'db.table' => 'myusers']); - - $response = $auth->login([ - 'username' => 'test-user', - 'password' => 'password' - ]); - - if (!$response) { - $this->fail(json_encode($auth->errors())); - } - - expect($response['user']['username'])->toBe('test-user'); -}); - -test('update should work with user defined table', function () { - $auth = auth(); - $auth->config(['session' => true, 'db.table' => 'myusers', 'session.lifetime' => '1 day']); - - $loginData = $auth->login([ - 'username' => 'test-user', - 'password' => 'password' - ]); - - if (!$loginData) { - $this->fail(json_encode($auth->errors())); - } - - $response = $auth->update([ - 'username' => 'test-user55', - 'email' => 'test-user55@example.com', - ]); - - if (!$response) { - $this->fail(json_encode($auth->errors())); - } - - expect($response['user']['username'])->toBe('test-user55'); - expect($response['user']['email'])->toBe('test-user55@example.com'); -}); - -test('user table can use uuid as id', function () { - createUsersTable('uuid_users', true); - - $auth = auth(); - $auth->config(['session' => false, 'db.table' => 'uuid_users']); - - $response = $auth->register([ - 'id' => '123e4567-e89b-12d3-a456-426614174000', - 'username' => 'test-user', - 'email' => 'test-user@example.com', - 'password' => 'password', - ]); - - if (!$response) { - $this->fail(json_encode($auth->errors())); - } - - expect($response['user']['username'])->toBe('test-user'); -}); diff --git a/tests/user.test.php b/tests/user.test.php new file mode 100644 index 0000000..840f884 --- /dev/null +++ b/tests/user.test.php @@ -0,0 +1,14 @@ + 'test-user', + 'password' => 'password' + ]; + + if (!$auth->login($userData)) { + $this->fail(json_encode($auth->errors())); + } + + expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class); +}); From e473d48e48deee78d75d7b1128c3661cf52dd2b1 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Fri, 25 Oct 2024 01:36:58 +0000 Subject: [PATCH 14/32] feat: implement login and hidden fields --- src/Auth.php | 36 +++++++++++++++++------------------- src/Auth/User.php | 24 +++++++++++++++++++++++- 2 files changed, 40 insertions(+), 20 deletions(-) diff --git a/src/Auth.php b/src/Auth.php index 01f4b46..8ae1dd5 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -118,11 +118,15 @@ public function login(array $credentials): bool unset($credentials[$passwordKey]); } - $user = $this->db->select($table)->where($credentials)->first(); + try { + $user = $this->db->select($table)->where($credentials)->first(); - if (!$user) { - $this->errorsArray['auth'] = Config::get('messages.loginParamsError'); - return false; + if (!$user) { + $this->errorsArray['auth'] = Config::get('messages.loginParamsError'); + return false; + } + } catch (\Throwable $th) { + throw new \Exception($th->getMessage()); } $passwordIsValid = (Config::get('password.verify') !== false && isset($user[$passwordKey])) @@ -136,9 +140,9 @@ public function login(array $credentials): bool return false; } - echo json_encode($user); + $this->user = new User($user); - return false; + return true; } /** @@ -245,21 +249,15 @@ public function user() $idKey = Config::get('id.key'); $table = Config::get('db.table'); - $user = $this->db->select($table)->where($idKey, $userId)->first(); - - if (!$user) { - $this->errorsArray = $this->db->errors(); - return null; - } - - $hidden = Config::get('hidden'); + try { + $user = $this->db->select($table)->where($idKey, $userId)->first(); - if (count($hidden) > 0) { - foreach ($hidden as $item) { - if (isset($user[$item])) { - unset($user[$item]); - } + if (!$user) { + $this->errorsArray = $this->db->errors(); + return null; } + } catch (\Throwable $th) { + throw new \Exception($th->getMessage()); } return $this->user = new User( diff --git a/src/Auth/User.php b/src/Auth/User.php index 45d5aed..5640620 100644 --- a/src/Auth/User.php +++ b/src/Auth/User.php @@ -59,8 +59,30 @@ public function id() */ public function getAuthInfo(): object { + $userData = $this->data; + + $idKey = Config::get('id.key'); + $hidden = Config::get('hidden'); + $passwordKey = Config::get('password.key'); + + if (count($hidden) > 0) { + foreach ($hidden as $item) { + if (isset($userData[$item])) { + unset($userData[$item]); + } + + if ($item === 'field.id' && isset($userData[$idKey])) { + unset($userData[$idKey]); + } + + if ($item === 'field.password' && isset($userData[$passwordKey])) { + unset($userData[$passwordKey]); + } + } + } + $dataToReturn = (object) [ - 'user' => $this->data, + 'user' => $userData, 'accessToken' => $this->tokens['access'], 'refreshToken' => $this->tokens['refresh'], ]; From 324dc1e827149e4898724022c9558c9e87d1c0ce Mon Sep 17 00:00:00 2001 From: mychidarko Date: Sun, 27 Oct 2024 10:19:35 +0000 Subject: [PATCH 15/32] feat: rework user + auth flows --- src/Auth.php | 193 ++++++++++++++++++++++++++++------------ src/Auth/Config.php | 7 +- src/Auth/User.php | 127 +++++++++++++++++++------- tests/Pest.php | 40 ++++++++- tests/core.test.php | 7 -- tests/login.test.php | 105 ++++++++++++++++++++++ tests/register.test.php | 92 +++++++++++++++++++ tests/session.test.php | 170 +++++++++++++++++++++++++++++++++++ tests/table.test.php | 93 +++++++++++++++++++ tests/update.test.php | 101 +++++++++++++++++++++ tests/user.test.php | 20 ++--- 11 files changed, 845 insertions(+), 110 deletions(-) delete mode 100644 tests/core.test.php create mode 100644 tests/login.test.php create mode 100644 tests/register.test.php create mode 100644 tests/session.test.php create mode 100644 tests/table.test.php create mode 100644 tests/update.test.php diff --git a/src/Auth.php b/src/Auth.php index 8ae1dd5..84544a9 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -32,12 +32,6 @@ class Auth */ protected $db; - /** - * Internal instance of Leaf session - * @var Session - */ - protected $session; - /** * All errors caught * @var array @@ -54,10 +48,6 @@ public function connect($dbConfig = []) $this->db = new Db(); $this->db->connect($dbConfig); - if (Config::get('session')) { - $this->initSession(Config::get('session.cookie')); - } - return $this; } @@ -72,10 +62,6 @@ public function autoConnect(array $pdoOptions = []) $this->db = new Db(); $this->db->autoConnect($pdoOptions); - if (Config::get('session')) { - $this->initSession(Config::get('session.cookie')); - } - return $this; } @@ -90,13 +76,29 @@ public function dbConnection(\PDO $connection) $this->db = new Db(); $this->db->connection($connection); - if (Config::get('session')) { - $this->initSession(Config::get('session.cookie')); + return $this; + } + + /** + * Get/Set Leaf Auth config + * + * @param string|array $config The auth config key or array of config + * @param mixed $value The value if $config is a string + */ + public function config($config, $value = null) + { + if (is_string($config) && $value === null) { + return Config::get($config); } - return $this; + Config::set( + is_string($config) + ? [$config => $value] + : $config + ); } + /** * Sign a user in * --- @@ -129,15 +131,17 @@ public function login(array $credentials): bool throw new \Exception($th->getMessage()); } - $passwordIsValid = (Config::get('password.verify') !== false && isset($user[$passwordKey])) - ? ((is_callable(Config::get('password.verify'))) - ? call_user_func(Config::get('password.verify'), $userPassword, $user[$passwordKey]) - : Password::verify($userPassword, $user[$passwordKey])) - : false; + if ($passwordKey !== false) { + $passwordIsValid = (Config::get('password.verify') !== false && isset($user[$passwordKey])) + ? ((is_callable(Config::get('password.verify'))) + ? call_user_func(Config::get('password.verify'), $userPassword, $user[$passwordKey]) + : Password::verify($userPassword, $user[$passwordKey])) + : false; - if (!$passwordIsValid) { - $this->errorsArray['password'] = Config::get('messages.loginPasswordError'); - return false; + if (!$passwordIsValid) { + $this->errorsArray['password'] = Config::get('messages.loginPasswordError'); + return false; + } } $this->user = new User($user); @@ -212,7 +216,93 @@ public function register(array $userData): bool public function update(array $userData): bool { $this->checkDbConnection(); - return false; + + $user = $this->user(); + + if (!$user) { + return false; + } + + $idKey = Config::get('id.key'); + $table = Config::get('db.table'); + + if (Config::get('timestamps')) { + $userData['updated_at'] = (new Date())->tick()->format(Config::get('timestamps.format')); + } + + try { + $query = $this->db->update($table)->params($userData)->where($idKey, $this->user->{$idKey})->unique($userData)->execute(); + + if (!$query) { + $this->errorsArray = array_merge($this->errorsArray, $this->db->errors()); + return false; + } + } catch (\Throwable $th) { + throw new \Exception($th->getMessage()); + } + + foreach ($userData as $key => $value) { + $this->user->{$key} = $value; + } + + return true; + } + + /** + * Update user password + * --- + * Update user password in the database + * + * @param string $oldPassword Old password + * @param string $newPassword New password + * @return bool + */ + public function updatePassword(string $oldPassword, string $newPassword): bool + { + $this->checkDbConnection(); + + $user = $this->user(); + + if (!$user) { + return false; + } + + $passwordKey = Config::get('password.key'); + + if (Config::get('password.verify') !== false && isset($user->{$passwordKey})) { + $passwordIsValid = (is_callable(Config::get('password.verify'))) + ? call_user_func(Config::get('password.verify'), $oldPassword, $user->{$passwordKey}) + : Password::verify($oldPassword, $user->{$passwordKey}); + + if (!$passwordIsValid) { + $this->errorsArray['password'] = Config::get('messages.loginPasswordError'); + return false; + } + } + + $newPassword = (Config::get('password.encode') !== false) + ? ((is_callable(Config::get('password.encode'))) + ? call_user_func(Config::get('password.encode'), $newPassword) + : Password::hash($newPassword)) + : $newPassword; + + try { + $query = $this->db->update(Config::get('db.table')) + ->params([$passwordKey => $newPassword]) + ->where(Config::get('id.key'), $this->id()) + ->execute(); + + if (!$query) { + $this->errorsArray = array_merge($this->errorsArray, $this->db->errors()); + return false; + } + } catch (\Throwable $th) { + throw new \Exception($th->getMessage()); + } + + $this->user->{$passwordKey} = $newPassword; + + return true; } /** @@ -221,13 +311,9 @@ public function update(array $userData): bool */ public function id() { - if ($this->user) { - return $this->user->id(); - } - return Config::get('session') ? $this->getFromSession('auth.id') - : ($this->parseToken()['user.id'] ?? null); + : ($this->user ? $this->user->id() : ($this->parseToken()['user.id'] ?? null)); } /** @@ -236,6 +322,14 @@ public function id() */ public function user() { + if (Config::get('session')) { + $userId = $this->getFromSession('auth.id'); + + if (!$userId) { + return null; + } + } + if ($this->user) { return $this->user; } @@ -297,6 +391,16 @@ public function parseToken() ); } + /** + * Return the current db instance + * + * @return Db + */ + public function db() + { + return $this->db; + } + protected function checkDbConnection(): void { if (!$this->db && function_exists('db')) { @@ -328,7 +432,7 @@ protected function sessionCheck() protected function isSessionExpired(): bool { - $sessionTtl = $this->session->get('session.ttl'); + $sessionTtl = Session::get('auth.ttl'); if (!$sessionTtl) { return false; @@ -337,35 +441,12 @@ protected function isSessionExpired(): bool $isSessionExpired = time() > $sessionTtl; if ($isSessionExpired) { - $this->session->unset('auth.user'); - $this->session->unset('auth.id'); - $this->session->unset('auth.token'); - $this->session->unset('session.startedAt'); - $this->session->unset('session.lastActivity'); - $this->session->unset('session.ttl'); + Session::unset('auth'); } return $isSessionExpired; } - protected function initSession(array $sessionCookieParams = []) - { - $session = new Session(false); - - if (!isset($_SESSION)) { - session_set_cookie_params($sessionCookieParams); - session_start(); - } - - if (!$session->has('session.startedAt')) { - $session->set('session.startedAt', time()); - } - - $session->set('session.lastActivity', time()); - - $this->session = $session; - } - protected function getTokenFromRequest() { $headers = null; diff --git a/src/Auth/Config.php b/src/Auth/Config.php index d7ef644..2cdd419 100644 --- a/src/Auth/Config.php +++ b/src/Auth/Config.php @@ -32,7 +32,6 @@ class Config 'session' => false, 'session.logout' => null, - 'session.register' => null, 'session.lifetime' => 60 * 60 * 24, 'session.cookie' => ['secure' => true, 'httponly' => true, 'samesite' => 'lax'], @@ -46,7 +45,7 @@ class Config /** * Set Leaf Auth config */ - public static function set($config) + public static function set($config): void { static::$config = array_merge(static::$config, $config); } @@ -54,7 +53,7 @@ public static function set($config) /** * Overwrite Leaf Auth config */ - public static function overwrite($config) + public static function overwrite($config): void { static::$config = $config; } @@ -62,7 +61,7 @@ public static function overwrite($config) /** * Get Leaf Auth config */ - public static function get($key = null) + public static function get($key = null): mixed { if ($key) { return static::$config[$key] ?? null; diff --git a/src/Auth/User.php b/src/Auth/User.php index 5640620..df7d543 100644 --- a/src/Auth/User.php +++ b/src/Auth/User.php @@ -3,7 +3,8 @@ namespace Leaf\Auth; use Firebase\JWT\JWT; -use Firebase\JWT\Key; +use Leaf\Http\Session; +use Leaf\Db; /** * Auth User @@ -17,6 +18,12 @@ class User { use UsesRoles; + /** + * Internal instance of Leaf session + * @var Session + */ + protected $session; + /** * User Information */ @@ -34,14 +41,40 @@ class User public function __construct($data) { - $accessTokenLifetime = Config::get('session') - ? Config::get('session.lifetime') - : Config::get('token.lifetime'); - $this->data = $data; - $this->tokens['access'] = $this->generateToken($accessTokenLifetime); - $this->tokens['refresh'] = $this->generateToken($accessTokenLifetime + 259200); + $sessionLifetime = Config::get('token.lifetime'); + + if (Config::get('session')) { + $sessionLifetime = Config::get('session.lifetime'); + + if (session_status() !== PHP_SESSION_ACTIVE) { + session_set_cookie_params(Config::get('session.cookie')); + session_start(); + } + + session_regenerate_id(); + + if (!Session::has('auth.startedAt')) { + Session::set('auth.startedAt', time()); + } + + Session::set('auth.lastActivity', time()); + Session::set('auth.id', $this->id()); + Session::set('auth.user', $this->get()); + + if ($sessionLifetime !== 0 && $sessionLifetime !== null) { + Session::set( + 'auth.ttl', + is_int($sessionLifetime) + ? time() + $sessionLifetime + : strtotime($sessionLifetime) ?? throw new \Exception('Invalid session lifetime') + ); + } + } + + $this->tokens['access'] = $this->generateToken($sessionLifetime); + $this->tokens['refresh'] = $this->generateToken($sessionLifetime + 259200); } /** @@ -59,30 +92,8 @@ public function id() */ public function getAuthInfo(): object { - $userData = $this->data; - - $idKey = Config::get('id.key'); - $hidden = Config::get('hidden'); - $passwordKey = Config::get('password.key'); - - if (count($hidden) > 0) { - foreach ($hidden as $item) { - if (isset($userData[$item])) { - unset($userData[$item]); - } - - if ($item === 'field.id' && isset($userData[$idKey])) { - unset($userData[$idKey]); - } - - if ($item === 'field.password' && isset($userData[$passwordKey])) { - unset($userData[$passwordKey]); - } - } - } - $dataToReturn = (object) [ - 'user' => $userData, + 'user' => $this->get(), 'accessToken' => $this->tokens['access'], 'refreshToken' => $this->tokens['refresh'], ]; @@ -124,16 +135,41 @@ public function generateToken($tokenLifetime): string public function get() { - return $this->data; + $userData = $this->data; + + $idKey = Config::get('id.key'); + $hidden = Config::get('hidden'); + $passwordKey = Config::get('password.key'); + + if (count($hidden) > 0) { + foreach ($hidden as $item) { + if (isset($userData[$item])) { + unset($userData[$item]); + } + + if ($item === 'field.id' && isset($userData[$idKey])) { + unset($userData[$idKey]); + } + + if ($item === 'field.password' && isset($userData[$passwordKey])) { + unset($userData[$passwordKey]); + } + } + } + + return $userData; } public function __toString() { - return json_encode($this->data); + return json_encode($this->get()); } public function __get($name) { + // using data instead of get() here because + // we want people to be able to user()->get hidden fields + // since it's expected to be used within the app return $this->data[$name] ?? null; } @@ -151,4 +187,31 @@ public function __unset($name) { unset($this->data[$name]); } + + /** + * Get a "user to many" table relation + * + * + * auth()->user()->orders()->get(); + * auth()->user()->transactions()->where('amount', '>', 100)->get(); + * auth()->user()->notes()->where('title', 'like', '%important%')->get(); + * auth()->user()->posts()->where('published', true)->get(); + * + * + * @param mixed $method The table to relate to + * @param mixed $args + * @throws \Exception + * @return Db + */ + public function __call($method, $args) + { + if (!class_exists('Leaf\App')) { + throw new \Exception('Relations are only available in Leaf apps.'); + } + + return auth() + ->db() + ->select($method) + ->where('user_id', $this->id()); + } } diff --git a/tests/Pest.php b/tests/Pest.php index 0688b0b..c2a5662 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -12,10 +12,48 @@ function getDatabaseConnection(): array ]; } -function connectToDatabase(): \Leaf\Db +function dbInstance(): \Leaf\Db { $db = new \Leaf\Db(); $db->connect(getDatabaseConnection()); return $db; } + +function authInstance(): \Leaf\Auth +{ + $auth = new \Leaf\Auth(); + $auth->dbConnection(dbInstance()->connection()); + + return $auth; +} + +function deleteUser(string $username, $table = 'users') +{ + $db = new \Leaf\Db(); + $db->connect(getDatabaseConnection()); + + $db->delete($table)->where('username', $username)->execute(); +} + +function createTableForUsers($table = 'users'): void +{ + $db = dbInstance(); + + try { + $db + ->query("CREATE TABLE IF NOT EXISTS $table ( + id SERIAL PRIMARY KEY, + username VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, + password VARCHAR(255) NOT NULL, + permissions JSONB, + roles JSONB, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + )") + ->execute(); + } catch (\Throwable $th) { + throw new \Exception("Failed to create table for users: " . $th->getMessage()); + } +} diff --git a/tests/core.test.php b/tests/core.test.php deleted file mode 100644 index 8a7d9ad..0000000 --- a/tests/core.test.php +++ /dev/null @@ -1,7 +0,0 @@ -toBeInstanceOf(\Leaf\Db::class); -}); diff --git a/tests/login.test.php b/tests/login.test.php new file mode 100644 index 0000000..009c362 --- /dev/null +++ b/tests/login.test.php @@ -0,0 +1,105 @@ +insert('users') + ->params([ + 'username' => 'test-user-login', + 'email' => 'test-user-login@example.com', + 'password' => password_hash('password', PASSWORD_BCRYPT) + ]) + ->execute(); + } catch (\Throwable $th) { + throw $th; + } +}); + +afterAll(function () { + dbInstance()->delete('users')->execute(); +}); + +test('user can login', function () { + $auth = authInstance(); + + $userData = [ + 'username' => 'test-user-login', + 'password' => 'password' + ]; + + $success = $auth->login($userData); + + expect($success)->toBeTrue(); + expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class); + expect($auth->user()->username)->toBe($userData['username']); +}); + +test('login generates tokens on success', function () { + $auth = authInstance(); + + $userData = [ + 'username' => 'test-user-login', + 'password' => 'password' + ]; + + $success = $auth->login($userData); + + expect($success)->toBeTrue(); + expect($auth->data())->not()->toBeNull(); + expect($auth->data()->accessToken)->toBeString(); + expect($auth->data()->refreshToken)->toBeString(); +}); + +test('login fails with incorrect password', function () { + $auth = authInstance(); + + $userData = [ + 'username' => 'test-user-login', + 'password' => 'wrong-password' + ]; + + $success = $auth->login($userData); + + $loginPasswordError = $auth->config('messages.loginPasswordError'); + + expect($success)->toBeFalse(); + expect($auth->user())->toBeNull(); + expect($auth->errors()['password'] ?? null)->toBe($loginPasswordError); +}); + +test('login should fail if user does not exist', function () { + $auth = authInstance(); + + $userData = [ + 'username' => 'non-existent-user', + 'password' => 'password' + ]; + + $success = $auth->login($userData); + + $loginParamsError = $auth->config('messages.loginParamsError'); + + expect($success)->toBeFalse(); + expect($auth->user())->toBeNull(); + expect($auth->errors()['auth'] ?? null)->toBe($loginParamsError); +}); + +test('login should work without password is password.key is false', function () { + $auth = authInstance(); + + $auth->config('password.key', false); + + $userData = [ + 'username' => 'test-user-login' + ]; + + $success = $auth->login($userData); + + expect($success)->toBeTrue(); + expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class); + expect($auth->user()->username)->toBe($userData['username']); + + $auth->config('password.key', 'password'); +}); diff --git a/tests/register.test.php b/tests/register.test.php new file mode 100644 index 0000000..00dd2b9 --- /dev/null +++ b/tests/register.test.php @@ -0,0 +1,92 @@ +delete('users')->where('username', 'test-user')->execute(); +}); + +test('user can register an account', function () { + $auth = authInstance(); + + $userData = [ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password' + ]; + + $success = $auth->register($userData); + + expect($success)->toBeTrue(); + expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class); + expect($auth->user()->username)->toBe($userData['username']); +}); + +test('user can login after registering', function () { + $auth = authInstance(); + + $userData = [ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password' + ]; + + $registerSuccess = $auth->register($userData); + + expect($registerSuccess)->toBeTrue(); + + $loginSuccess = $auth->login($userData); + + expect($loginSuccess)->toBeTrue(); + expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class); + expect($auth->user()->username)->toBe($userData['username']); +}); + +test('user can only sign up once', function () { + $auth = authInstance(); + + $auth->config([ + 'unique' => ['email', 'username'] + ]); + + $userData = [ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password' + ]; + + $registerSuccess = $auth->register($userData); + + expect($registerSuccess)->toBeTrue(); + + $registerAgain = $auth->register($userData); + + expect($registerAgain)->toBeFalse(); + + expect($auth->errors())->toBe([ + 'email' => 'email already exists', + 'username' => 'username already exists', + ]); +}); + +test('register passwords are encrypted', function () { + $auth = authInstance(); + + $auth->config([ + 'hidden' => [] + ]); + + $userData = [ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password' + ]; + + $registerSuccess = $auth->register($userData); + + expect($registerSuccess)->toBeTrue(); + expect($auth->user()->password)->not()->toBe($userData['password']); + expect(password_verify($userData['password'], $auth->user()->password))->toBeTrue(); +}); diff --git a/tests/session.test.php b/tests/session.test.php new file mode 100644 index 0000000..dcd6998 --- /dev/null +++ b/tests/session.test.php @@ -0,0 +1,170 @@ +delete('users')->execute(); +}); + +test('register should create a new session when session => true', function () { + $auth = authInstance(); + $auth->config(['session' => true]); + + $userData = [ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password' + ]; + + $success = $auth->register($userData); + + expect($success)->toBeTrue(); + expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class); + expect($auth->user()->username)->toBe($userData['username']); + + expect(session_status())->toBe(PHP_SESSION_ACTIVE); + expect($_SESSION['auth']['user']['username'] ?? null)->toBe($userData['username']); +}); + +test('register should not create a new session when session => false', function () { + $auth = authInstance(); + $auth->config(['session' => false]); + + $userData = [ + 'username' => 'test-user2', + 'email' => 'test-user2@example.com', + 'password' => 'password' + ]; + + $success = $auth->register($userData); + + expect($success)->toBeTrue(); + expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class); + + expect(session_status())->toBe(PHP_SESSION_NONE); + expect($_SESSION['auth']['user']['username'] ?? null)->toBeNull(); +}); + +test('login should create session when session => true', function () { + $auth = authInstance(); + $auth->config(['session' => true]); + + $userData = [ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password' + ]; + + $success = $auth->login($userData); + + expect($success)->toBeTrue(); + expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class); + expect($auth->user()->username)->toBe($userData['username']); + + expect(session_status())->toBe(PHP_SESSION_ACTIVE); + expect($_SESSION['auth']['user']['username'] ?? null)->toBe($userData['username']); +}); + +test('session should create auth.ttl when session.lifetime is not 0', function () { + $auth = authInstance(); + $auth->config(['session' => true, 'session.lifetime' => 2]); + + $userData = [ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password' + ]; + + $timeBeforeLogin = time(); + + $success = $auth->login($userData); + + expect($success)->toBeTrue(); + expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class); + expect($auth->user()->username)->toBe($userData['username']); + + expect(session_status())->toBe(PHP_SESSION_ACTIVE); + expect($_SESSION['auth']['user']['username'] ?? null)->toBe($userData['username']); + + expect($_SESSION['auth']['ttl'])->toBeGreaterThan($timeBeforeLogin); +}); + +test('session should not create auth.ttl when session.lifetime is 0', function () { + $auth = authInstance(); + $auth->config(['session' => true, 'session.lifetime' => 0]); + + $userData = [ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password' + ]; + + $success = $auth->login($userData); + + expect($success)->toBeTrue(); + expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class); + expect($auth->user()->username)->toBe($userData['username']); + + expect(session_status())->toBe(PHP_SESSION_ACTIVE); + expect($_SESSION['auth']['user']['username'] ?? null)->toBe($userData['username']); + + expect($_SESSION['auth']['ttl'] ?? null)->toBeNull(); +}); + +test('session should expire after session.lifetime', function () { + $auth = authInstance(); + $auth->config(['session' => true, 'session.lifetime' => 2]); + + $userData = [ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password' + ]; + + $success = $auth->login($userData); + + expect($success)->toBeTrue(); + expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class); + expect($auth->user()->username)->toBe($userData['username']); + + expect(session_status())->toBe(PHP_SESSION_ACTIVE); + expect($_SESSION['auth']['user']['username'] ?? null)->toBe($userData['username']); + + sleep(3); + + expect($auth->id())->toBeNull(); + expect($auth->user())->toBeNull(); +}); + +test('login should regenerate session id when session => true and session is already active', function () { + $auth = authInstance(); + $auth->config(['session' => true]); + + session_start(); + + $sessionId = session_id(); + + $userData = [ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password' + ]; + + $success = $auth->login($userData); + + $newSessionId = session_id(); + + expect($success)->toBeTrue(); + expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class); + expect($auth->user()->username)->toBe($userData['username']); + + expect(session_status())->toBe(PHP_SESSION_ACTIVE); + expect($_SESSION['auth']['user']['username'] ?? null)->toBe($userData['username']); + + expect($newSessionId)->not()->toBe($sessionId); +}); diff --git a/tests/table.test.php b/tests/table.test.php new file mode 100644 index 0000000..d07f6de --- /dev/null +++ b/tests/table.test.php @@ -0,0 +1,93 @@ +config(['session' => false, 'db.table' => 'myusers']); + + $success = $auth->register([ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password', + ]); + + if (!$success) { + $this->fail(json_encode($auth->errors())); + } + + expect($auth->user()->username)->toBe('test-user'); +})->skip(); + +test('login should work with user defined table', function () { + $auth = authInstance(); + $auth->config(['session' => false, 'db.table' => 'myusers']); + + $success = $auth->login([ + 'username' => 'test-user', + 'password' => 'password' + ]); + + if (!$success) { + $this->fail(json_encode($auth->errors())); + } + + expect($auth->user()->username)->toBe('test-user'); +})->skip(); + +test('update should work with user defined table', function () { + $auth = authInstance(); + $auth->config(['session' => true, 'db.table' => 'myusers', 'session.lifetime' => '1 day']); + + $success = $auth->login([ + 'username' => 'test-user', + 'password' => 'password' + ]); + + if (!$success) { + $this->fail(json_encode($auth->errors())); + } + + $response = $auth->update([ + 'username' => 'test-user55', + 'email' => 'test-user55@example.com', + ]); + + if (!$response) { + $this->fail(json_encode($auth->errors())); + } + + expect($response['user']['username'])->toBe('test-user55'); + expect($response['user']['email'])->toBe('test-user55@example.com'); +})->skip(); + +test('user table can use uuid as id', function () { + createUsersTable('uuid_users', true); + + $auth = authInstance(); + $auth->config(['session' => false, 'db.table' => 'uuid_users']); + + $response = $auth->register([ + 'id' => '123e4567-e89b-12d3-a456-426614174000', + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password', + ]); + + if (!$response) { + $this->fail(json_encode($auth->errors())); + } + + expect($response['user']['username'])->toBe('test-user'); +})->skip(); diff --git a/tests/update.test.php b/tests/update.test.php new file mode 100644 index 0000000..e79f2ef --- /dev/null +++ b/tests/update.test.php @@ -0,0 +1,101 @@ +register([ + 'username' => 'test-user-1', + 'email' => 'test-user-1@example.com', + 'password' => 'password', + ]); + + $auth->register([ + 'username' => 'test-user-2', + 'email' => 'test-user-2@example.com', + 'password' => 'password', + ]); + + sleep(1); +}); + +// afterAll(function () { +// dbInstance()->delete('users')->execute(); +// }); + +test('update should update user data', function () { + $auth = authInstance(); + + $success = $auth->login([ + 'username' => 'test-user-1', + 'password' => 'password' + ]); + + if (!$success) { + $this->fail(json_encode($auth->errors())); + } + + $updateData = [ + 'username' => 'test-user-3', + ]; + + $updateSuccess = $auth->update($updateData); + + if (!$updateSuccess) { + $this->fail(json_encode($auth->errors())); + } + + expect($auth->user()->username)->toBe($updateData['username']); +}); + +test('update should fail if user already exists', function () { + $auth = authInstance(); + + $success = $auth->login([ + 'username' => 'test-user-3', + 'password' => 'password' + ]); + + if (!$success) { + $this->fail(json_encode($auth->errors())); + } + + $updateData = [ + 'username' => 'test-user-2', + ]; + + $updateSuccess = $auth->update($updateData); + + expect($updateSuccess)->toBeFalse(); + expect($auth->errors()['username'])->toBe('username already exists'); +}); + +test('updatePassword should update user password', function () { + $auth = authInstance(); + $auth->config(['unique' => ['username']]); + + $success = $auth->login([ + 'username' => 'test-user-2', + 'password' => 'password' + ]); + + if (!$success) { + $this->fail(json_encode($auth->errors())); + } + + $oldPassword = 'password'; + $newPassword = 'new-password'; + + $updateSuccess = $auth->updatePassword($oldPassword, $newPassword); + + if (!$updateSuccess) { + $this->fail(json_encode($auth->errors())); + } + + $loginSuccess = $auth->login([ + 'username' => 'test-user-2', + 'password' => 'new-password' + ]); + + expect($loginSuccess)->toBeTrue(); + expect($auth->user()->{$auth->config('password.key')})->not()->toBe($oldPassword); +}); diff --git a/tests/user.test.php b/tests/user.test.php index 840f884..0178848 100644 --- a/tests/user.test.php +++ b/tests/user.test.php @@ -1,14 +1,14 @@ 'test-user', - 'password' => 'password' - ]; +// test('auth user is instance of Leaf\Auth\User', function () { +// $userData = [ +// 'username' => 'test-user', +// 'password' => 'password' +// ]; - if (!$auth->login($userData)) { - $this->fail(json_encode($auth->errors())); - } +// if (!$auth->login($userData)) { +// $this->fail(json_encode($auth->errors())); +// } - expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class); -}); +// expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class); +// }); From 9c7d08a0ca1b7adbb9332dcecc9b5e17c3c81086 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Sun, 27 Oct 2024 20:03:59 +0000 Subject: [PATCH 16/32] feat: add support for uniques --- src/Auth.php | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/Auth.php b/src/Auth.php index 84544a9..883f08a 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -230,8 +230,26 @@ public function update(array $userData): bool $userData['updated_at'] = (new Date())->tick()->format(Config::get('timestamps.format')); } + if (count(Config::get('unique')) > 0) { + foreach (Config::get('unique') as $unique) { + if (!isset($userData[$unique])) { + continue; + } + + $data = $this->db->select($table, Config::get('id.key'))->where($unique, $userData[$unique])->first(); + + if ($data && $data[Config::get('id.key')] !== $this->id()) { + $this->errorsArray[$unique] = "$unique already exists"; + } + } + + if (count($this->errorsArray) > 0) { + return false; + } + } + try { - $query = $this->db->update($table)->params($userData)->where($idKey, $this->user->{$idKey})->unique($userData)->execute(); + $query = $this->db->update($table)->params($userData)->where($idKey, $this->user->{$idKey})->execute(); if (!$query) { $this->errorsArray = array_merge($this->errorsArray, $this->db->errors()); @@ -241,6 +259,10 @@ public function update(array $userData): bool throw new \Exception($th->getMessage()); } + if (Config::get('session')) { + session_regenerate_id(); + } + foreach ($userData as $key => $value) { $this->user->{$key} = $value; } @@ -416,7 +438,7 @@ protected function checkDbConnection(): void protected function getFromSession($value) { - if ($this->isSessionExpired()) { + if ($this->checkAndExpireSession()) { return null; } @@ -430,7 +452,7 @@ protected function sessionCheck() } } - protected function isSessionExpired(): bool + protected function checkAndExpireSession(): bool { $sessionTtl = Session::get('auth.ttl'); From 390e2ca5f80ff33836a67e7470ecb90f38e23609 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Sun, 27 Oct 2024 20:04:27 +0000 Subject: [PATCH 17/32] feat: add support for user relations --- src/Auth/User.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Auth/User.php b/src/Auth/User.php index df7d543..1774c0c 100644 --- a/src/Auth/User.php +++ b/src/Auth/User.php @@ -192,10 +192,10 @@ public function __unset($name) * Get a "user to many" table relation * * - * auth()->user()->orders()->get(); + * auth()->user()->orders()->all(); * auth()->user()->transactions()->where('amount', '>', 100)->get(); * auth()->user()->notes()->where('title', 'like', '%important%')->get(); - * auth()->user()->posts()->where('published', true)->get(); + * auth()->user()->posts()->where('published', true)->all(); * * * @param mixed $method The table to relate to From efedbc14b0b42421d9acfa6f79765b4e8cfcaa9f Mon Sep 17 00:00:00 2001 From: mychidarko Date: Sun, 27 Oct 2024 20:04:32 +0000 Subject: [PATCH 18/32] test: update tests --- tests/table.test.php | 8 +++----- tests/update.test.php | 35 ++++++++++++++++++++++++++++++++--- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/tests/table.test.php b/tests/table.test.php index d07f6de..ce71717 100644 --- a/tests/table.test.php +++ b/tests/table.test.php @@ -8,9 +8,7 @@ }); afterAll(function () { - deleteUser('test-user', 'myusers'); - deleteUser('test-user55', 'myusers'); - deleteUser('test-user', 'uuid_users'); + dbInstance()->delete('myusers')->execute(); }); test('register should save user in user defined table', function () { @@ -28,7 +26,7 @@ } expect($auth->user()->username)->toBe('test-user'); -})->skip(); +}); test('login should work with user defined table', function () { $auth = authInstance(); @@ -44,7 +42,7 @@ } expect($auth->user()->username)->toBe('test-user'); -})->skip(); +}); test('update should work with user defined table', function () { $auth = authInstance(); diff --git a/tests/update.test.php b/tests/update.test.php index e79f2ef..a23727f 100644 --- a/tests/update.test.php +++ b/tests/update.test.php @@ -18,9 +18,9 @@ sleep(1); }); -// afterAll(function () { -// dbInstance()->delete('users')->execute(); -// }); +afterAll(function () { + dbInstance()->delete('users')->execute(); +}); test('update should update user data', function () { $auth = authInstance(); @@ -99,3 +99,32 @@ expect($loginSuccess)->toBeTrue(); expect($auth->user()->{$auth->config('password.key')})->not()->toBe($oldPassword); }); + +test('update should regenerate session id if session => true', function () { + $auth = authInstance(); + $auth->config(['session' => true]); + + $success = $auth->login([ + 'username' => 'test-user-2', + 'password' => 'new-password' + ]); + + if (!$success) { + $this->fail(json_encode($auth->errors())); + } + + $updateData = [ + 'username' => 'test-user-5', + ]; + + $initialSessionId = session_id(); + + $updateSuccess = $auth->update($updateData); + + if (!$updateSuccess) { + $this->fail(json_encode($auth->errors())); + } + + expect($auth->user()->username)->toBe($updateData['username']); + expect($initialSessionId)->not()->toBe(session_id()); +}); From 957aff2f342e796e31592062b4f210b86b6cd00e Mon Sep 17 00:00:00 2001 From: mychidarko Date: Sun, 27 Oct 2024 20:17:39 +0000 Subject: [PATCH 19/32] feat: add support for leaf middleware --- src/Auth.php | 46 +++++++++++++++++++++++++++++++++++++++++++++- src/Auth/User.php | 9 +++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/Auth.php b/src/Auth.php index 883f08a..1b7c8c4 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -98,7 +98,6 @@ public function config($config, $value = null) ); } - /** * Sign a user in * --- @@ -396,6 +395,51 @@ public function data() return $user->getAuthInfo(); } + /** + * Get generated access tokens + * @return array|null + */ + public function tokens() + { + $user = $this->user(); + + if (!$user) { + return null; + } + + return $user->tokens(); + } + + /** + * Register auth middleware for your Leaf apps + * @param string $middleware The middleware to register + * @param callable $callback The callback to run if middleware fails + */ + public function middleware(string $middleware, callable $callback) + { + if (!class_exists(\Leaf\App::class)) { + throw new \Exception('This feature is only available for Leaf apps'); + } + + if ($middleware === 'auth.required') { + return app()->registerMiddleware('auth.required', function () use ($callback) { + if (!$this->user()) { + $callback(); + } + }); + } + + if ($middleware === 'auth.guest') { + return app()->registerMiddleware('auth.guest', function () use ($callback) { + if ($this->user()) { + $callback(); + } + }); + } + + app()->registerMiddleware($middleware, $callback); + } + /** * Parse the current user's token */ diff --git a/src/Auth/User.php b/src/Auth/User.php index 1774c0c..5bc1f92 100644 --- a/src/Auth/User.php +++ b/src/Auth/User.php @@ -109,6 +109,15 @@ public function getAuthInfo(): object return $dataToReturn; } + /** + * Return generated tokens + * @return array + */ + public function tokens(): array + { + return $this->tokens; + } + /** * Generate a new JWT for the user * @return string From ae43d704a3fa9ba8ca6c39ba7d2537983eb53a37 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Thu, 31 Oct 2024 00:04:23 +0000 Subject: [PATCH 20/32] feat: add logout implementation --- src/Auth.php | 31 ++++++++++++++++++++++++++++++- src/Auth/Config.php | 1 - 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/Auth.php b/src/Auth.php index 1b7c8c4..b97b016 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -326,6 +326,35 @@ public function updatePassword(string $oldPassword, string $newPassword): bool return true; } + /** + * Sign a user out + * --- + * Sign out the currently authenticated user + * + * @param string|array|callable|null $redirectUrl Redirect to this url after logout + * @return bool + */ + public function logout($action = null): bool + { + if (Config::get('session')) { + Session::unset('auth'); + } + + $this->user = null; + + if (is_callable($action)) { + $action($this); + return true; + } + + if ($action) { + response()->redirect($action); + exit; + } + + return true; + } + /** * Get the id of the currently authenticated user * @return string|int @@ -544,7 +573,7 @@ protected function getTokenFromRequest() protected function getTokenFromSession() { - return \Leaf\Http\Session::get('auth.token'); + return Session::get('auth.token'); } /** diff --git a/src/Auth/Config.php b/src/Auth/Config.php index 2cdd419..d92f98f 100644 --- a/src/Auth/Config.php +++ b/src/Auth/Config.php @@ -31,7 +31,6 @@ class Config 'hidden' => ['field.id', 'field.password'], 'session' => false, - 'session.logout' => null, 'session.lifetime' => 60 * 60 * 24, 'session.cookie' => ['secure' => true, 'httponly' => true, 'samesite' => 'lax'], From 44db12a645e5cc16e0aa69778f151e0d6dd40e43 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Thu, 31 Oct 2024 00:05:57 +0000 Subject: [PATCH 21/32] chore: disable roles --- src/Auth/User.php | 11 ++--------- src/Auth/UsesRoles.php | 5 +++++ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/Auth/User.php b/src/Auth/User.php index 5bc1f92..37c1ae0 100644 --- a/src/Auth/User.php +++ b/src/Auth/User.php @@ -16,8 +16,6 @@ */ class User { - use UsesRoles; - /** * Internal instance of Leaf session * @var Session @@ -34,11 +32,6 @@ class User */ protected array $tokens = []; - /** - * User Roles - */ - protected array $roles = []; - public function __construct($data) { $this->data = $data; @@ -98,11 +91,11 @@ public function getAuthInfo(): object 'refreshToken' => $this->tokens['refresh'], ]; - if (count($this->roles)) { + if (count($this->roles ?? [])) { $dataToReturn->roles = $this->roles; } - if (count($this->permissions)) { + if (count($this->permissions ?? [])) { $dataToReturn->permissions = $this->permissions; } diff --git a/src/Auth/UsesRoles.php b/src/Auth/UsesRoles.php index 8ff8099..7e82582 100644 --- a/src/Auth/UsesRoles.php +++ b/src/Auth/UsesRoles.php @@ -17,6 +17,11 @@ trait UsesRoles { */ protected array $permissions = []; + /** + * User Roles + */ + protected array $roles = []; + /** * Grant a user permission to do something * @param string|array $permission The permission to grant From 36c5a2fbc427b6d2ea706368a168877284dbb905 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Thu, 31 Oct 2024 00:06:17 +0000 Subject: [PATCH 22/32] test: refactor test-user from tests --- tests/Pest.php | 6 ++++ tests/db.test.php | 0 tests/login.test.php | 32 ++++++----------- tests/register.test.php | 40 +++++---------------- tests/session.test.php | 80 ++++++++++++++++------------------------- tests/table.test.php | 28 +++++---------- tests/user.test.php | 68 ++++++++++++++++++++++++++++------- 7 files changed, 120 insertions(+), 134 deletions(-) create mode 100644 tests/db.test.php diff --git a/tests/Pest.php b/tests/Pest.php index c2a5662..cc1c37f 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,5 +1,11 @@ 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password' +]]]); + function getDatabaseConnection(): array { return [ diff --git a/tests/db.test.php b/tests/db.test.php new file mode 100644 index 0000000..e69de29 diff --git a/tests/login.test.php b/tests/login.test.php index 009c362..ddd7d9b 100644 --- a/tests/login.test.php +++ b/tests/login.test.php @@ -7,8 +7,8 @@ dbInstance() ->insert('users') ->params([ - 'username' => 'test-user-login', - 'email' => 'test-user-login@example.com', + 'username' => 'test-user', + 'email' => 'test-user@example.com', 'password' => password_hash('password', PASSWORD_BCRYPT) ]) ->execute(); @@ -21,42 +21,32 @@ dbInstance()->delete('users')->execute(); }); -test('user can login', function () { +test('user can login', function (array $testUser) { $auth = authInstance(); - $userData = [ - 'username' => 'test-user-login', - 'password' => 'password' - ]; - - $success = $auth->login($userData); + $success = $auth->login($testUser); expect($success)->toBeTrue(); expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class); - expect($auth->user()->username)->toBe($userData['username']); -}); + expect($auth->user()->username)->toBe($testUser['username']); +})->with('test-user'); -test('login generates tokens on success', function () { +test('login generates tokens on success', function (array $testUser) { $auth = authInstance(); - $userData = [ - 'username' => 'test-user-login', - 'password' => 'password' - ]; - - $success = $auth->login($userData); + $success = $auth->login($testUser); expect($success)->toBeTrue(); expect($auth->data())->not()->toBeNull(); expect($auth->data()->accessToken)->toBeString(); expect($auth->data()->refreshToken)->toBeString(); -}); +})->with('test-user'); test('login fails with incorrect password', function () { $auth = authInstance(); $userData = [ - 'username' => 'test-user-login', + 'username' => 'test-user', 'password' => 'wrong-password' ]; @@ -92,7 +82,7 @@ $auth->config('password.key', false); $userData = [ - 'username' => 'test-user-login' + 'username' => 'test-user' ]; $success = $auth->login($userData); diff --git a/tests/register.test.php b/tests/register.test.php index 00dd2b9..6968720 100644 --- a/tests/register.test.php +++ b/tests/register.test.php @@ -8,30 +8,18 @@ dbInstance()->delete('users')->where('username', 'test-user')->execute(); }); -test('user can register an account', function () { +test('user can register an account', function (array $userData) { $auth = authInstance(); - - $userData = [ - 'username' => 'test-user', - 'email' => 'test-user@example.com', - 'password' => 'password' - ]; $success = $auth->register($userData); expect($success)->toBeTrue(); expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class); expect($auth->user()->username)->toBe($userData['username']); -}); +})->with('test-user'); -test('user can login after registering', function () { +test('user can login after registering', function (array $userData) { $auth = authInstance(); - - $userData = [ - 'username' => 'test-user', - 'email' => 'test-user@example.com', - 'password' => 'password' - ]; $registerSuccess = $auth->register($userData); @@ -42,20 +30,14 @@ expect($loginSuccess)->toBeTrue(); expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class); expect($auth->user()->username)->toBe($userData['username']); -}); +})->with('test-user'); -test('user can only sign up once', function () { +test('user can only sign up once', function (array $userData) { $auth = authInstance(); $auth->config([ 'unique' => ['email', 'username'] ]); - - $userData = [ - 'username' => 'test-user', - 'email' => 'test-user@example.com', - 'password' => 'password' - ]; $registerSuccess = $auth->register($userData); @@ -69,24 +51,18 @@ 'email' => 'email already exists', 'username' => 'username already exists', ]); -}); +})->with('test-user'); -test('register passwords are encrypted', function () { +test('register passwords are encrypted', function (array $userData) { $auth = authInstance(); $auth->config([ 'hidden' => [] ]); - - $userData = [ - 'username' => 'test-user', - 'email' => 'test-user@example.com', - 'password' => 'password' - ]; $registerSuccess = $auth->register($userData); expect($registerSuccess)->toBeTrue(); expect($auth->user()->password)->not()->toBe($userData['password']); expect(password_verify($userData['password'], $auth->user()->password))->toBeTrue(); -}); +})->with('test-user'); diff --git a/tests/session.test.php b/tests/session.test.php index dcd6998..a8edc09 100644 --- a/tests/session.test.php +++ b/tests/session.test.php @@ -11,16 +11,10 @@ dbInstance()->delete('users')->execute(); }); -test('register should create a new session when session => true', function () { +test('register should create a new session when session => true', function (array $userData) { $auth = authInstance(); $auth->config(['session' => true]); - $userData = [ - 'username' => 'test-user', - 'email' => 'test-user@example.com', - 'password' => 'password' - ]; - $success = $auth->register($userData); expect($success)->toBeTrue(); @@ -29,9 +23,9 @@ expect(session_status())->toBe(PHP_SESSION_ACTIVE); expect($_SESSION['auth']['user']['username'] ?? null)->toBe($userData['username']); -}); +})->with('test-user'); -test('register should not create a new session when session => false', function () { +test('register should not create a new session when session => false', function (array $userData) { $auth = authInstance(); $auth->config(['session' => false]); @@ -48,18 +42,12 @@ expect(session_status())->toBe(PHP_SESSION_NONE); expect($_SESSION['auth']['user']['username'] ?? null)->toBeNull(); -}); +})->with('test-user'); -test('login should create session when session => true', function () { +test('login should create session when session => true', function (array $userData) { $auth = authInstance(); $auth->config(['session' => true]); - $userData = [ - 'username' => 'test-user', - 'email' => 'test-user@example.com', - 'password' => 'password' - ]; - $success = $auth->login($userData); expect($success)->toBeTrue(); @@ -68,18 +56,12 @@ expect(session_status())->toBe(PHP_SESSION_ACTIVE); expect($_SESSION['auth']['user']['username'] ?? null)->toBe($userData['username']); -}); +})->with('test-user'); -test('session should create auth.ttl when session.lifetime is not 0', function () { +test('session should create auth.ttl when session.lifetime is not 0', function (array $userData) { $auth = authInstance(); $auth->config(['session' => true, 'session.lifetime' => 2]); - $userData = [ - 'username' => 'test-user', - 'email' => 'test-user@example.com', - 'password' => 'password' - ]; - $timeBeforeLogin = time(); $success = $auth->login($userData); @@ -92,18 +74,12 @@ expect($_SESSION['auth']['user']['username'] ?? null)->toBe($userData['username']); expect($_SESSION['auth']['ttl'])->toBeGreaterThan($timeBeforeLogin); -}); +})->with('test-user'); -test('session should not create auth.ttl when session.lifetime is 0', function () { +test('session should not create auth.ttl when session.lifetime is 0', function (array $userData) { $auth = authInstance(); $auth->config(['session' => true, 'session.lifetime' => 0]); - $userData = [ - 'username' => 'test-user', - 'email' => 'test-user@example.com', - 'password' => 'password' - ]; - $success = $auth->login($userData); expect($success)->toBeTrue(); @@ -114,18 +90,12 @@ expect($_SESSION['auth']['user']['username'] ?? null)->toBe($userData['username']); expect($_SESSION['auth']['ttl'] ?? null)->toBeNull(); -}); +})->with('test-user'); -test('session should expire after session.lifetime', function () { +test('session should expire after session.lifetime', function (array $userData) { $auth = authInstance(); $auth->config(['session' => true, 'session.lifetime' => 2]); - $userData = [ - 'username' => 'test-user', - 'email' => 'test-user@example.com', - 'password' => 'password' - ]; - $success = $auth->login($userData); expect($success)->toBeTrue(); @@ -139,9 +109,9 @@ expect($auth->id())->toBeNull(); expect($auth->user())->toBeNull(); -}); +})->with('test-user'); -test('login should regenerate session id when session => true and session is already active', function () { +test('login should regenerate session id when session => true and session is already active', function (array $userData) { $auth = authInstance(); $auth->config(['session' => true]); @@ -149,12 +119,6 @@ $sessionId = session_id(); - $userData = [ - 'username' => 'test-user', - 'email' => 'test-user@example.com', - 'password' => 'password' - ]; - $success = $auth->login($userData); $newSessionId = session_id(); @@ -167,4 +131,20 @@ expect($_SESSION['auth']['user']['username'] ?? null)->toBe($userData['username']); expect($newSessionId)->not()->toBe($sessionId); -}); +})->with('test-user'); + +test('logout should remove auth info from session when session => true', function (array $userData) { + $auth = authInstance(); + $auth->config(['session' => true]); + + $success = $auth->login($userData); + + expect($success)->toBeTrue(); + expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class); + expect($_SESSION['auth']['user']['username'] ?? null)->toBe($userData['username']); + + $auth->logout(); + + expect($auth->user())->toBeNull(); + expect($_SESSION['auth']['user']['username'] ?? null)->toBeNull(); +})->with('test-user'); diff --git a/tests/table.test.php b/tests/table.test.php index ce71717..35e7e05 100644 --- a/tests/table.test.php +++ b/tests/table.test.php @@ -11,47 +11,37 @@ dbInstance()->delete('myusers')->execute(); }); -test('register should save user in user defined table', function () { +test('register should save user in user defined table', function (array $testUser) { $auth = authInstance(); $auth->config(['session' => false, 'db.table' => 'myusers']); - $success = $auth->register([ - 'username' => 'test-user', - 'email' => 'test-user@example.com', - 'password' => 'password', - ]); + $success = $auth->register($testUser); if (!$success) { $this->fail(json_encode($auth->errors())); } expect($auth->user()->username)->toBe('test-user'); -}); +})->with('test-user'); -test('login should work with user defined table', function () { +test('login should work with user defined table', function (array $testUser) { $auth = authInstance(); $auth->config(['session' => false, 'db.table' => 'myusers']); - $success = $auth->login([ - 'username' => 'test-user', - 'password' => 'password' - ]); + $success = $auth->login($testUser); if (!$success) { $this->fail(json_encode($auth->errors())); } expect($auth->user()->username)->toBe('test-user'); -}); +})->with('test-user'); -test('update should work with user defined table', function () { +test('update should work with user defined table', function (array $testUser) { $auth = authInstance(); $auth->config(['session' => true, 'db.table' => 'myusers', 'session.lifetime' => '1 day']); - $success = $auth->login([ - 'username' => 'test-user', - 'password' => 'password' - ]); + $success = $auth->login($testUser); if (!$success) { $this->fail(json_encode($auth->errors())); @@ -68,7 +58,7 @@ expect($response['user']['username'])->toBe('test-user55'); expect($response['user']['email'])->toBe('test-user55@example.com'); -})->skip(); +})->with('test-user')->skip(); test('user table can use uuid as id', function () { createUsersTable('uuid_users', true); diff --git a/tests/user.test.php b/tests/user.test.php index 0178848..0f94b3c 100644 --- a/tests/user.test.php +++ b/tests/user.test.php @@ -1,14 +1,58 @@ 'test-user', -// 'password' => 'password' -// ]; - -// if (!$auth->login($userData)) { -// $this->fail(json_encode($auth->errors())); -// } - -// expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class); -// }); +beforeAll(function () { + createTableForUsers(); + + try { + dbInstance() + ->insert('users') + ->params([ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => password_hash('password', PASSWORD_BCRYPT) + ]) + ->execute(); + } catch (\Throwable $th) { + throw $th; + } +}); + +afterAll(function () { + dbInstance()->delete('users')->execute(); +}); + +test('auth user is instance of Leaf\Auth\User', function (array $testUser) { + $auth = authInstance(); + $auth->config(['db.table' => 'users']); + + $success = $auth->login($testUser); + + if (!$success) { + $this->fail(json_encode($auth->errors())); + } + + expect($success)->toBeTrue(); + expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class); + expect($auth->user()->username)->toBe($testUser['username']); +})->with('test-user'); + +test('logout can use logout callback to run custom action', function (array $testUser) { + $auth = authInstance(); + $auth->config(['db.table' => 'users']); + + $success = $auth->login($testUser); + + if (!$success) { + $this->fail(json_encode($auth->errors())); + } + + expect($success)->toBeTrue(); + expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class); + expect($auth->user()->username)->toBe($testUser['username']); + + $auth->logout(function ($auth) use ($testUser) { + expect($auth)->toBeInstanceOf(\Leaf\Auth::class); + }); + + expect($auth->user())->toBeNull(); +})->with('test-user'); From 3df88c6e8ed4212411bf804a8257ae52f5115e12 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Thu, 31 Oct 2024 00:08:24 +0000 Subject: [PATCH 23/32] chore: generate actions from alchemy --- .github/workflows/lint.yml | 34 ++++++++++++++++++++++++++++++++++ .github/workflows/tests.yml | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..ddd79f9 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,34 @@ +name: Lint code + +on: [push] + +jobs: + lint: + runs-on: ubuntu-latest + strategy: + fail-fast: true + + steps: + - name: Checkout code + uses: actions/checkout@v2 + with: + ref: ${{ github.head_ref }} + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + extensions: json, zip, dom, curl, libxml, mbstring + tools: composer:v2 + coverage: none + + - name: Install PHP dependencies + run: composer update --no-interaction --no-progress + + - name: Run Linter + run: composer run lint + + - name: Commit changes + uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: 'chore: fix styling' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..67522e6 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,32 @@ +name: Run Tests + +on: ["push","pull_request"] + +jobs: + tests: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + os: ["ubuntu-latest","macos-latest","windows-latest"] + php: ["8.3","8.2","8.1","8.0","7.4"] + + name: PHP ${{ matrix.php }} - ${{ matrix.os }} + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: json, zip, dom, curl, libxml, mbstring + tools: composer:v2 + coverage: xdebug + + - name: Install PHP dependencies + run: composer update --no-interaction --no-progress + + - name: Run Tests + run: composer run test -- --flags=coverage From 09eb3edab04458196512ef19267e380be5fab3a2 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Thu, 31 Oct 2024 00:08:42 +0000 Subject: [PATCH 24/32] chore: fix styling --- src/Auth.php | 18 +++++++++--------- src/Auth/Config.php | 2 +- src/Auth/User.php | 6 +++--- src/Auth/UsesRoles.php | 6 +++--- tests/Pest.php | 4 ++-- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/Auth.php b/src/Auth.php index b97b016..8221ba0 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -67,7 +67,7 @@ public function autoConnect(array $pdoOptions = []) /** * Pass in db connection instance directly - * + * * @param \PDO $connection A connection instance of your db * @return $this; */ @@ -81,7 +81,7 @@ public function dbConnection(\PDO $connection) /** * Get/Set Leaf Auth config - * + * * @param string|array $config The auth config key or array of config * @param mixed $value The value if $config is a string */ @@ -102,7 +102,7 @@ public function config($config, $value = null) * Sign a user in * --- * Verify user credentials and sign them in with token or session - * + * * @param array $credentials User credentials * @return bool */ @@ -152,7 +152,7 @@ public function login(array $credentials): bool * Register a new user * --- * Save a new user to the database - * + * * @param array $userData User data * @return bool */ @@ -208,7 +208,7 @@ public function register(array $userData): bool * Update user data * --- * Update user data in the database - * + * * @param array $userData User data * @return bool */ @@ -273,7 +273,7 @@ public function update(array $userData): bool * Update user password * --- * Update user password in the database - * + * * @param string $oldPassword Old password * @param string $newPassword New password * @return bool @@ -330,7 +330,7 @@ public function updatePassword(string $oldPassword, string $newPassword): bool * Sign a user out * --- * Sign out the currently authenticated user - * + * * @param string|array|callable|null $redirectUrl Redirect to this url after logout * @return bool */ @@ -465,7 +465,7 @@ public function middleware(string $middleware, callable $callback) } }); } - + app()->registerMiddleware($middleware, $callback); } @@ -488,7 +488,7 @@ public function parseToken() /** * Return the current db instance - * + * * @return Db */ public function db() diff --git a/src/Auth/Config.php b/src/Auth/Config.php index d92f98f..d76db92 100644 --- a/src/Auth/Config.php +++ b/src/Auth/Config.php @@ -6,7 +6,7 @@ * Config for Leaf Auth * -------- * Set/Get config to match your app - * + * * @since 3.0.0 * @version 0.1.0 */ diff --git a/src/Auth/User.php b/src/Auth/User.php index 37c1ae0..65e6e4e 100644 --- a/src/Auth/User.php +++ b/src/Auth/User.php @@ -10,7 +10,7 @@ * Auth User * ---- * Class representing a user - * + * * @since 3.0.0 * @version 1.0.0 */ @@ -192,14 +192,14 @@ public function __unset($name) /** * Get a "user to many" table relation - * + * * * auth()->user()->orders()->all(); * auth()->user()->transactions()->where('amount', '>', 100)->get(); * auth()->user()->notes()->where('title', 'like', '%important%')->get(); * auth()->user()->posts()->where('published', true)->all(); * - * + * * @param mixed $method The table to relate to * @param mixed $args * @throws \Exception diff --git a/src/Auth/UsesRoles.php b/src/Auth/UsesRoles.php index 7e82582..3650afa 100644 --- a/src/Auth/UsesRoles.php +++ b/src/Auth/UsesRoles.php @@ -6,12 +6,12 @@ * Functionality for user permissions * ---- * Addition to user class - * + * * @version 0.1.0 * @since 3.0.0 */ -trait UsesRoles { - +trait UsesRoles +{ /** * User Permissions */ diff --git a/tests/Pest.php b/tests/Pest.php index cc1c37f..06ecfcd 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -45,7 +45,7 @@ function deleteUser(string $username, $table = 'users') function createTableForUsers($table = 'users'): void { $db = dbInstance(); - + try { $db ->query("CREATE TABLE IF NOT EXISTS $table ( @@ -60,6 +60,6 @@ function createTableForUsers($table = 'users'): void )") ->execute(); } catch (\Throwable $th) { - throw new \Exception("Failed to create table for users: " . $th->getMessage()); + throw new \Exception('Failed to create table for users: ' . $th->getMessage()); } } From 44dfa9d8428e810f41fa74ac148b4e05babf1a79 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Thu, 31 Oct 2024 00:22:52 +0000 Subject: [PATCH 25/32] test: update tests --- tests/register.test.php | 7 ++++++- tests/session.test.php | 5 +++++ tests/user.test.php | 1 + 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/register.test.php b/tests/register.test.php index 6968720..8457031 100644 --- a/tests/register.test.php +++ b/tests/register.test.php @@ -2,10 +2,11 @@ beforeAll(function () { createTableForUsers(); + dbInstance()->delete('users')->execute(); }); afterEach(function () { - dbInstance()->delete('users')->where('username', 'test-user')->execute(); + dbInstance()->delete('users')->execute(); }); test('user can register an account', function (array $userData) { @@ -13,6 +14,10 @@ $success = $auth->register($userData); + if (!$success) { + $this->fail(json_encode($auth->errors())); + } + expect($success)->toBeTrue(); expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class); expect($auth->user()->username)->toBe($userData['username']); diff --git a/tests/session.test.php b/tests/session.test.php index a8edc09..3f699eb 100644 --- a/tests/session.test.php +++ b/tests/session.test.php @@ -1,5 +1,10 @@ delete('users')->execute(); +}); + afterEach(function () { if (session_status() === PHP_SESSION_ACTIVE) { $_SESSION = []; diff --git a/tests/user.test.php b/tests/user.test.php index 0f94b3c..71eab7c 100644 --- a/tests/user.test.php +++ b/tests/user.test.php @@ -2,6 +2,7 @@ beforeAll(function () { createTableForUsers(); + dbInstance()->delete('users')->execute(); try { dbInstance() From 9d965dc70c8e1300375560ebac02592e810c4280 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Thu, 31 Oct 2024 00:41:48 +0000 Subject: [PATCH 26/32] test: update tests --- .github/workflows/tests.yml | 2 +- alchemy.yml | 2 +- tests/login.test.php | 20 ++++++++-- tests/register.test.php | 40 ++++++++++++++++---- tests/session.test.php | 74 +++++++++++++++++++++++++++++-------- tests/table.test.php | 30 ++++++++++----- tests/user.test.php | 20 ++++++++-- 7 files changed, 145 insertions(+), 43 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 67522e6..f5c018f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,7 +21,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: json, zip, dom, curl, libxml, mbstring + extensions: json, zip, dom, curl, libxml, mbstring, PDO_PGSQL tools: composer:v2 coverage: xdebug diff --git a/alchemy.yml b/alchemy.yml index aa85f3c..8f6f49a 100644 --- a/alchemy.yml +++ b/alchemy.yml @@ -25,7 +25,7 @@ actions: - macos-latest - windows-latest php: - extensions: json, zip, dom, curl, libxml, mbstring + extensions: json, zip, dom, curl, libxml, mbstring, PDO_PGSQL versions: - '8.3' - '8.2' diff --git a/tests/login.test.php b/tests/login.test.php index ddd7d9b..954826a 100644 --- a/tests/login.test.php +++ b/tests/login.test.php @@ -21,26 +21,38 @@ dbInstance()->delete('users')->execute(); }); -test('user can login', function (array $testUser) { +test('user can login', function () { $auth = authInstance(); + $testUser = [ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password' + ]; + $success = $auth->login($testUser); expect($success)->toBeTrue(); expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class); expect($auth->user()->username)->toBe($testUser['username']); -})->with('test-user'); +}); -test('login generates tokens on success', function (array $testUser) { +test('login generates tokens on success', function () { $auth = authInstance(); + $testUser = [ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password' + ]; + $success = $auth->login($testUser); expect($success)->toBeTrue(); expect($auth->data())->not()->toBeNull(); expect($auth->data()->accessToken)->toBeString(); expect($auth->data()->refreshToken)->toBeString(); -})->with('test-user'); +}); test('login fails with incorrect password', function () { $auth = authInstance(); diff --git a/tests/register.test.php b/tests/register.test.php index 8457031..59b712a 100644 --- a/tests/register.test.php +++ b/tests/register.test.php @@ -9,9 +9,15 @@ dbInstance()->delete('users')->execute(); }); -test('user can register an account', function (array $userData) { +test('user can register an account', function () { $auth = authInstance(); + $userData = [ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password' + ]; + $success = $auth->register($userData); if (!$success) { @@ -21,11 +27,17 @@ expect($success)->toBeTrue(); expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class); expect($auth->user()->username)->toBe($userData['username']); -})->with('test-user'); +}); -test('user can login after registering', function (array $userData) { +test('user can login after registering', function () { $auth = authInstance(); + $userData = [ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password' + ]; + $registerSuccess = $auth->register($userData); expect($registerSuccess)->toBeTrue(); @@ -35,11 +47,17 @@ expect($loginSuccess)->toBeTrue(); expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class); expect($auth->user()->username)->toBe($userData['username']); -})->with('test-user'); +}); -test('user can only sign up once', function (array $userData) { +test('user can only sign up once', function () { $auth = authInstance(); + $userData = [ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password' + ]; + $auth->config([ 'unique' => ['email', 'username'] ]); @@ -56,11 +74,17 @@ 'email' => 'email already exists', 'username' => 'username already exists', ]); -})->with('test-user'); +}); -test('register passwords are encrypted', function (array $userData) { +test('register passwords are encrypted', function () { $auth = authInstance(); + $userData = [ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password' + ]; + $auth->config([ 'hidden' => [] ]); @@ -70,4 +94,4 @@ expect($registerSuccess)->toBeTrue(); expect($auth->user()->password)->not()->toBe($userData['password']); expect(password_verify($userData['password'], $auth->user()->password))->toBeTrue(); -})->with('test-user'); +}); diff --git a/tests/session.test.php b/tests/session.test.php index 3f699eb..2fb5022 100644 --- a/tests/session.test.php +++ b/tests/session.test.php @@ -16,10 +16,16 @@ dbInstance()->delete('users')->execute(); }); -test('register should create a new session when session => true', function (array $userData) { +test('register should create a new session when session => true', function () { $auth = authInstance(); $auth->config(['session' => true]); + $userData = [ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password' + ]; + $success = $auth->register($userData); expect($success)->toBeTrue(); @@ -28,9 +34,9 @@ expect(session_status())->toBe(PHP_SESSION_ACTIVE); expect($_SESSION['auth']['user']['username'] ?? null)->toBe($userData['username']); -})->with('test-user'); +}); -test('register should not create a new session when session => false', function (array $userData) { +test('register should not create a new session when session => false', function () { $auth = authInstance(); $auth->config(['session' => false]); @@ -47,12 +53,18 @@ expect(session_status())->toBe(PHP_SESSION_NONE); expect($_SESSION['auth']['user']['username'] ?? null)->toBeNull(); -})->with('test-user'); +}); -test('login should create session when session => true', function (array $userData) { +test('login should create session when session => true', function () { $auth = authInstance(); $auth->config(['session' => true]); + $userData = [ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password' + ]; + $success = $auth->login($userData); expect($success)->toBeTrue(); @@ -61,12 +73,18 @@ expect(session_status())->toBe(PHP_SESSION_ACTIVE); expect($_SESSION['auth']['user']['username'] ?? null)->toBe($userData['username']); -})->with('test-user'); +}); -test('session should create auth.ttl when session.lifetime is not 0', function (array $userData) { +test('session should create auth.ttl when session.lifetime is not 0', function () { $auth = authInstance(); $auth->config(['session' => true, 'session.lifetime' => 2]); + $userData = [ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password' + ]; + $timeBeforeLogin = time(); $success = $auth->login($userData); @@ -79,12 +97,18 @@ expect($_SESSION['auth']['user']['username'] ?? null)->toBe($userData['username']); expect($_SESSION['auth']['ttl'])->toBeGreaterThan($timeBeforeLogin); -})->with('test-user'); +}); -test('session should not create auth.ttl when session.lifetime is 0', function (array $userData) { +test('session should not create auth.ttl when session.lifetime is 0', function () { $auth = authInstance(); $auth->config(['session' => true, 'session.lifetime' => 0]); + $userData = [ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password' + ]; + $success = $auth->login($userData); expect($success)->toBeTrue(); @@ -95,12 +119,18 @@ expect($_SESSION['auth']['user']['username'] ?? null)->toBe($userData['username']); expect($_SESSION['auth']['ttl'] ?? null)->toBeNull(); -})->with('test-user'); +}); -test('session should expire after session.lifetime', function (array $userData) { +test('session should expire after session.lifetime', function () { $auth = authInstance(); $auth->config(['session' => true, 'session.lifetime' => 2]); + $userData = [ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password' + ]; + $success = $auth->login($userData); expect($success)->toBeTrue(); @@ -114,9 +144,9 @@ expect($auth->id())->toBeNull(); expect($auth->user())->toBeNull(); -})->with('test-user'); +}); -test('login should regenerate session id when session => true and session is already active', function (array $userData) { +test('login should regenerate session id when session => true and session is already active', function () { $auth = authInstance(); $auth->config(['session' => true]); @@ -124,6 +154,12 @@ $sessionId = session_id(); + $userData = [ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password' + ]; + $success = $auth->login($userData); $newSessionId = session_id(); @@ -136,12 +172,18 @@ expect($_SESSION['auth']['user']['username'] ?? null)->toBe($userData['username']); expect($newSessionId)->not()->toBe($sessionId); -})->with('test-user'); +}); -test('logout should remove auth info from session when session => true', function (array $userData) { +test('logout should remove auth info from session when session => true', function () { $auth = authInstance(); $auth->config(['session' => true]); + $userData = [ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password' + ]; + $success = $auth->login($userData); expect($success)->toBeTrue(); @@ -152,4 +194,4 @@ expect($auth->user())->toBeNull(); expect($_SESSION['auth']['user']['username'] ?? null)->toBeNull(); -})->with('test-user'); +}); diff --git a/tests/table.test.php b/tests/table.test.php index 35e7e05..ff235cd 100644 --- a/tests/table.test.php +++ b/tests/table.test.php @@ -11,37 +11,49 @@ dbInstance()->delete('myusers')->execute(); }); -test('register should save user in user defined table', function (array $testUser) { +test('register should save user in user defined table', function () { $auth = authInstance(); $auth->config(['session' => false, 'db.table' => 'myusers']); - $success = $auth->register($testUser); + $success = $auth->register([ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password' + ]); if (!$success) { $this->fail(json_encode($auth->errors())); } expect($auth->user()->username)->toBe('test-user'); -})->with('test-user'); +}); -test('login should work with user defined table', function (array $testUser) { +test('login should work with user defined table', function () { $auth = authInstance(); $auth->config(['session' => false, 'db.table' => 'myusers']); - $success = $auth->login($testUser); + $success = $auth->login([ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password' + ]); if (!$success) { $this->fail(json_encode($auth->errors())); } expect($auth->user()->username)->toBe('test-user'); -})->with('test-user'); +}); -test('update should work with user defined table', function (array $testUser) { +test('update should work with user defined table', function () { $auth = authInstance(); $auth->config(['session' => true, 'db.table' => 'myusers', 'session.lifetime' => '1 day']); - $success = $auth->login($testUser); + $success = $auth->login([ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password' + ]); if (!$success) { $this->fail(json_encode($auth->errors())); @@ -58,7 +70,7 @@ expect($response['user']['username'])->toBe('test-user55'); expect($response['user']['email'])->toBe('test-user55@example.com'); -})->with('test-user')->skip(); +})->skip(); test('user table can use uuid as id', function () { createUsersTable('uuid_users', true); diff --git a/tests/user.test.php b/tests/user.test.php index 71eab7c..4086518 100644 --- a/tests/user.test.php +++ b/tests/user.test.php @@ -22,10 +22,16 @@ dbInstance()->delete('users')->execute(); }); -test('auth user is instance of Leaf\Auth\User', function (array $testUser) { +test('auth user is instance of Leaf\Auth\User', function () { $auth = authInstance(); $auth->config(['db.table' => 'users']); + $testUser = [ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password' + ]; + $success = $auth->login($testUser); if (!$success) { @@ -35,12 +41,18 @@ expect($success)->toBeTrue(); expect($auth->user())->toBeInstanceOf(\Leaf\Auth\User::class); expect($auth->user()->username)->toBe($testUser['username']); -})->with('test-user'); +}); -test('logout can use logout callback to run custom action', function (array $testUser) { +test('logout can use logout callback to run custom action', function () { $auth = authInstance(); $auth->config(['db.table' => 'users']); + $testUser = [ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => 'password' + ]; + $success = $auth->login($testUser); if (!$success) { @@ -56,4 +68,4 @@ }); expect($auth->user())->toBeNull(); -})->with('test-user'); +}); From ececd7934a77be887311d1f13b82a4d3ed29624d Mon Sep 17 00:00:00 2001 From: mychidarko Date: Thu, 31 Oct 2024 00:48:37 +0000 Subject: [PATCH 27/32] test: update test setup --- .github/workflows/tests.yml | 2 +- alchemy.yml | 2 -- src/Auth/Config.php | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f5c018f..bfd99e9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,7 +8,7 @@ jobs: strategy: fail-fast: true matrix: - os: ["ubuntu-latest","macos-latest","windows-latest"] + os: ["ubuntu-latest"] php: ["8.3","8.2","8.1","8.0","7.4"] name: PHP ${{ matrix.php }} - ${{ matrix.os }} diff --git a/alchemy.yml b/alchemy.yml index 8f6f49a..1177beb 100644 --- a/alchemy.yml +++ b/alchemy.yml @@ -22,8 +22,6 @@ actions: - tests os: - ubuntu-latest - - macos-latest - - windows-latest php: extensions: json, zip, dom, curl, libxml, mbstring, PDO_PGSQL versions: diff --git a/src/Auth/Config.php b/src/Auth/Config.php index d76db92..dfa9e04 100644 --- a/src/Auth/Config.php +++ b/src/Auth/Config.php @@ -60,7 +60,7 @@ public static function overwrite($config): void /** * Get Leaf Auth config */ - public static function get($key = null): mixed + public static function get($key = null) { if ($key) { return static::$config[$key] ?? null; From 8322bc584a2c8d2a40de1dc1a236746fbee822fe Mon Sep 17 00:00:00 2001 From: mychidarko Date: Thu, 31 Oct 2024 00:55:06 +0000 Subject: [PATCH 28/32] test: update db.table config --- tests/register.test.php | 4 ++++ tests/session.test.php | 8 ++++++++ tests/update.test.php | 4 ++++ 3 files changed, 16 insertions(+) diff --git a/tests/register.test.php b/tests/register.test.php index 59b712a..630335f 100644 --- a/tests/register.test.php +++ b/tests/register.test.php @@ -11,6 +11,7 @@ test('user can register an account', function () { $auth = authInstance(); + $auth->config(['db.table' => 'users']); $userData = [ 'username' => 'test-user', @@ -31,6 +32,7 @@ test('user can login after registering', function () { $auth = authInstance(); + $auth->config(['db.table' => 'users']); $userData = [ 'username' => 'test-user', @@ -51,6 +53,7 @@ test('user can only sign up once', function () { $auth = authInstance(); + $auth->config(['db.table' => 'users']); $userData = [ 'username' => 'test-user', @@ -78,6 +81,7 @@ test('register passwords are encrypted', function () { $auth = authInstance(); + $auth->config(['db.table' => 'users']); $userData = [ 'username' => 'test-user', diff --git a/tests/session.test.php b/tests/session.test.php index 2fb5022..55a3c2c 100644 --- a/tests/session.test.php +++ b/tests/session.test.php @@ -19,6 +19,7 @@ test('register should create a new session when session => true', function () { $auth = authInstance(); $auth->config(['session' => true]); + $auth->config(['db.table' => 'users']); $userData = [ 'username' => 'test-user', @@ -39,6 +40,7 @@ test('register should not create a new session when session => false', function () { $auth = authInstance(); $auth->config(['session' => false]); + $auth->config(['db.table' => 'users']); $userData = [ 'username' => 'test-user2', @@ -58,6 +60,7 @@ test('login should create session when session => true', function () { $auth = authInstance(); $auth->config(['session' => true]); + $auth->config(['db.table' => 'users']); $userData = [ 'username' => 'test-user', @@ -78,6 +81,7 @@ test('session should create auth.ttl when session.lifetime is not 0', function () { $auth = authInstance(); $auth->config(['session' => true, 'session.lifetime' => 2]); + $auth->config(['db.table' => 'users']); $userData = [ 'username' => 'test-user', @@ -102,6 +106,7 @@ test('session should not create auth.ttl when session.lifetime is 0', function () { $auth = authInstance(); $auth->config(['session' => true, 'session.lifetime' => 0]); + $auth->config(['db.table' => 'users']); $userData = [ 'username' => 'test-user', @@ -124,6 +129,7 @@ test('session should expire after session.lifetime', function () { $auth = authInstance(); $auth->config(['session' => true, 'session.lifetime' => 2]); + $auth->config(['db.table' => 'users']); $userData = [ 'username' => 'test-user', @@ -149,6 +155,7 @@ test('login should regenerate session id when session => true and session is already active', function () { $auth = authInstance(); $auth->config(['session' => true]); + $auth->config(['db.table' => 'users']); session_start(); @@ -177,6 +184,7 @@ test('logout should remove auth info from session when session => true', function () { $auth = authInstance(); $auth->config(['session' => true]); + $auth->config(['db.table' => 'users']); $userData = [ 'username' => 'test-user', diff --git a/tests/update.test.php b/tests/update.test.php index a23727f..9495ee4 100644 --- a/tests/update.test.php +++ b/tests/update.test.php @@ -24,6 +24,7 @@ test('update should update user data', function () { $auth = authInstance(); + $auth->config(['db.table' => 'users']); $success = $auth->login([ 'username' => 'test-user-1', @@ -49,6 +50,7 @@ test('update should fail if user already exists', function () { $auth = authInstance(); + $auth->config(['db.table' => 'users']); $success = $auth->login([ 'username' => 'test-user-3', @@ -72,6 +74,7 @@ test('updatePassword should update user password', function () { $auth = authInstance(); $auth->config(['unique' => ['username']]); + $auth->config(['db.table' => 'users']); $success = $auth->login([ 'username' => 'test-user-2', @@ -103,6 +106,7 @@ test('update should regenerate session id if session => true', function () { $auth = authInstance(); $auth->config(['session' => true]); + $auth->config(['db.table' => 'users']); $success = $auth->login([ 'username' => 'test-user-2', From fb68b709c6abe516e718513ed06fa4b449ae56c7 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Thu, 31 Oct 2024 08:58:07 +0000 Subject: [PATCH 29/32] fix: patch up encode running on false password.key --- src/Auth.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Auth.php b/src/Auth.php index 8221ba0..9d43740 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -162,10 +162,11 @@ public function register(array $userData): bool $table = Config::get('db.table'); $passwordKey = Config::get('password.key'); + $passwordEncode = Config::get('password.encode'); - if (Config::get('password.encode') !== false) { - $userData[$passwordKey] = (is_callable(Config::get('password.encode'))) - ? call_user_func(Config::get('password.encode'), $userData[$passwordKey]) + if ($passwordEncode !== false && $passwordKey !== false) { + $userData[$passwordKey] = (is_callable($passwordEncode)) + ? call_user_func($passwordEncode, $userData[$passwordKey]) : Password::hash($userData[$passwordKey]); } From 55d02bdd4695380c19a00d46e7d958257f3d3bda Mon Sep 17 00:00:00 2001 From: mychidarko Date: Thu, 31 Oct 2024 08:58:58 +0000 Subject: [PATCH 30/32] fix: patch up ?? throw for 7.4 --- src/Auth/User.php | 17 +++++++++++------ tests/update.test.php | 1 + 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Auth/User.php b/src/Auth/User.php index 65e6e4e..785420e 100644 --- a/src/Auth/User.php +++ b/src/Auth/User.php @@ -57,12 +57,17 @@ public function __construct($data) Session::set('auth.user', $this->get()); if ($sessionLifetime !== 0 && $sessionLifetime !== null) { - Session::set( - 'auth.ttl', - is_int($sessionLifetime) - ? time() + $sessionLifetime - : strtotime($sessionLifetime) ?? throw new \Exception('Invalid session lifetime') - ); + if (!is_int($sessionLifetime)) { + $sessionLifetime = strtotime($sessionLifetime); + + if (!$sessionLifetime) { + throw new \Exception('Invalid session lifetime'); + } + } else { + $sessionLifetime = time() + $sessionLifetime; + } + + Session::set('auth.ttl', $sessionLifetime); } } diff --git a/tests/update.test.php b/tests/update.test.php index 9495ee4..76b2286 100644 --- a/tests/update.test.php +++ b/tests/update.test.php @@ -2,6 +2,7 @@ beforeAll(function () { $auth = authInstance(); + $auth->config(['db.table' => 'users']); $auth->register([ 'username' => 'test-user-1', From 95e4636468ad28a5383692af7bdd822d38ebeec2 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Sat, 2 Nov 2024 12:54:30 +0000 Subject: [PATCH 31/32] chore: remove test action --- .github/workflows/tests.yml | 32 -------------------------------- 1 file changed, 32 deletions(-) delete mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index bfd99e9..0000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Run Tests - -on: ["push","pull_request"] - -jobs: - tests: - runs-on: ${{ matrix.os }} - strategy: - fail-fast: true - matrix: - os: ["ubuntu-latest"] - php: ["8.3","8.2","8.1","8.0","7.4"] - - name: PHP ${{ matrix.php }} - ${{ matrix.os }} - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - extensions: json, zip, dom, curl, libxml, mbstring, PDO_PGSQL - tools: composer:v2 - coverage: xdebug - - - name: Install PHP dependencies - run: composer update --no-interaction --no-progress - - - name: Run Tests - run: composer run test -- --flags=coverage From 33400b884a9ab96aaacc6187e024fd6a96c89337 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Wed, 6 Nov 2024 23:28:52 +0000 Subject: [PATCH 32/32] chore: update alchemy version --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 6fe7bc9..e5ce7b8 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,6 @@ "leafs/db": "*", "leafs/form": "*", "leafs/http": "*", - "leafs/alchemy": "dev-next", "firebase/php-jwt": "^6.10" }, "config": { @@ -48,7 +47,8 @@ }, "require-dev": { "pestphp/pest": "^1.0 | ^2.0", - "friendsofphp/php-cs-fixer": "^3.64" + "friendsofphp/php-cs-fixer": "^3.64", + "leafs/alchemy": "^2.0" }, "scripts": { "test": "./vendor/bin/alchemy setup --test",