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');
+});