diff --git a/database/.gitkeep b/database/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/database/2023_01_09_213225_create_users_tables.php b/database/2023_01_09_213225_create_users_tables.php
new file mode 100644
index 0000000..815b2c9
--- /dev/null
+++ b/database/2023_01_09_213225_create_users_tables.php
@@ -0,0 +1,104 @@
+execute('
+ CREATE TABLE hau_users (
+ user_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+ user_name VARCHAR(255) NOT NULL COLLATE \'ascii_general_ci\',
+ user_country CHAR(2) NOT NULL DEFAULT \'XX\' COLLATE \'ascii_general_ci\',
+ user_colour INT(10) UNSIGNED NULL DEFAULT NULL,
+ user_super TINYINT(1) UNSIGNED NOT NULL DEFAULT \'0\',
+ user_time_zone VARBINARY(255) NOT NULL DEFAULT \'UTC\',
+ user_created TIMESTAMP NOT NULL DEFAULT current_timestamp(),
+ user_updated TIMESTAMP NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
+ user_deleted TIMESTAMP NULL DEFAULT NULL,
+ PRIMARY KEY (user_id) USING BTREE,
+ UNIQUE INDEX hau_users_name_unique (user_name) USING BTREE,
+ INDEX hau_users_created_index (user_created) USING BTREE,
+ INDEX hau_users_deleted_index (user_deleted) USING BTREE
+ ) ENGINE=InnoDB COLLATE=utf8mb4_bin;
+ ');
+
+ $conn->execute('
+ CREATE TABLE hau_users_auth (
+ user_id INT(10) UNSIGNED NOT NULL,
+ user_auth_type VARCHAR(32) NOT NULL COLLATE \'ascii_general_ci\',
+ user_auth_enabled TIMESTAMP NOT NULL DEFAULT current_timestamp(),
+ PRIMARY KEY (user_id, user_auth_type) USING BTREE,
+ INDEX hau_users_auth_user_foreign (user_id) USING BTREE,
+ CONSTRAINT hau_users_auth_user_foreign
+ FOREIGN KEY (user_id)
+ REFERENCES hau_users (user_id)
+ ON UPDATE CASCADE
+ ON DELETE CASCADE
+ ) ENGINE=InnoDB COLLATE=utf8mb4_bin;
+ ');
+
+ $conn->execute('
+ CREATE TABLE hau_users_backup (
+ user_id INT(10) UNSIGNED NOT NULL,
+ user_backup_code BINARY(8) NOT NULL,
+ user_backup_created TIMESTAMP NOT NULL DEFAULT current_timestamp(),
+ user_backup_used TIMESTAMP NULL DEFAULT NULL,
+ PRIMARY KEY (user_id, user_backup_code) USING BTREE,
+ INDEX hau_users_backup_used_index (user_backup_used) USING BTREE,
+ CONSTRAINT hau_users_backup_user_foreign
+ FOREIGN KEY (user_id)
+ REFERENCES hau_users (user_id)
+ ON UPDATE CASCADE
+ ON DELETE CASCADE
+ ) ENGINE=InnoDB COLLATE=utf8mb4_bin;
+ ');
+
+ $conn->execute('
+ CREATE TABLE hau_users_emails (
+ user_email_address VARCHAR(255) NOT NULL COLLATE \'ascii_general_ci\',
+ user_id INT(10) UNSIGNED NOT NULL,
+ user_email_created TIMESTAMP NOT NULL DEFAULT current_timestamp(),
+ user_email_verified TIMESTAMP NULL DEFAULT NULL,
+ user_email_recovery TINYINT(1) UNSIGNED NOT NULL DEFAULT \'0\',
+ PRIMARY KEY (user_email_address) USING BTREE,
+ UNIQUE INDEX hau_users_emails_user_foreign (user_id) USING BTREE,
+ INDEX hau_users_emails_created_index (user_email_created) USING BTREE,
+ INDEX hau_users_emails_recovery_index (user_email_recovery) USING BTREE,
+ INDEX hau_users_emails_verified_index (user_email_verified) USING BTREE,
+ CONSTRAINT hau_users_emails_user_foreign
+ FOREIGN KEY (user_id)
+ REFERENCES hau_users (user_id)
+ ON UPDATE CASCADE
+ ON DELETE CASCADE
+ ) ENGINE=InnoDB COLLATE=utf8mb4_bin;
+ ');
+
+ $conn->execute('
+ CREATE TABLE hau_users_passwords (
+ user_id INT(10) UNSIGNED NOT NULL,
+ user_password_hash VARBINARY(255) NOT NULL,
+ user_password_changed TIMESTAMP NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
+ UNIQUE INDEX hau_users_passwords_user_foreign (user_id) USING BTREE,
+ CONSTRAINT hau_users_passwords_user_foreign
+ FOREIGN KEY (user_id)
+ REFERENCES hau_users (user_id)
+ ON UPDATE CASCADE
+ ON DELETE CASCADE
+ ) ENGINE=InnoDB COLLATE=utf8mb4_bin;
+ ');
+
+ $conn->execute('
+ CREATE TABLE hau_users_totp (
+ user_id INT(10) UNSIGNED NOT NULL,
+ user_totp_key BINARY(26) NOT NULL,
+ user_totp_changed TIMESTAMP NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
+ UNIQUE INDEX hau_users_totp_user_foreign (user_id) USING BTREE,
+ CONSTRAINT hau_users_totp_user_foreign
+ FOREIGN KEY (user_id)
+ REFERENCES hau_users (user_id)
+ ON UPDATE CASCADE
+ ON DELETE CASCADE
+ ) ENGINE=InnoDB COLLATE=utf8mb4_bin;
+ ');
+ }
+}
diff --git a/hanyuu.php b/hanyuu.php
index da8ea05..f75dcb5 100644
--- a/hanyuu.php
+++ b/hanyuu.php
@@ -3,6 +3,8 @@ namespace Hanyuu;
use Index\Autoloader;
use Index\Environment;
+use Index\Data\DbTools;
+use Hanyuu\Config\IConfig;
use Hanyuu\Config\ArrayConfig;
define('HAU_STARTUP', microtime(true));
@@ -44,5 +46,9 @@ set_exception_handler(function(\Throwable $ex) {
die('
Hanyuu is sad.
');
});
-$hau = new HanyuuContext(ArrayConfig::open(HAU_DIR_CONFIG . '/config.ini'));
-$hau->connectDb();
+$cfg = ArrayConfig::open(HAU_DIR_CONFIG . '/config.ini');
+
+$dbc = DbTools::create($cfg->getValue('database:dsn', IConfig::T_STR, 'null'));
+$dbc->execute('SET SESSION time_zone = \'+00:00\', sql_mode = \'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION\';');
+
+$hau = new HanyuuContext($cfg, $dbc);
diff --git a/src/HanyuuContext.php b/src/HanyuuContext.php
index f64ff92..ead856c 100644
--- a/src/HanyuuContext.php
+++ b/src/HanyuuContext.php
@@ -2,7 +2,6 @@
namespace Hanyuu;
use Index\Data\IDbConnection;
-use Index\Data\DbTools;
use Index\Data\Migration\IDbMigrationRepo;
use Index\Data\Migration\DbMigrationManager;
use Index\Data\Migration\FsDbMigrationRepo;
@@ -11,29 +10,30 @@ use Index\Http\HttpRequest;
use Index\Routing\IRouter;
use Hanyuu\Config\IConfig;
use Hanyuu\Templating\TemplateContext;
+use Hanyuu\Users\IUsers;
+use Hanyuu\Users\Db\DbUsers;
class HanyuuContext {
- private const DB_INIT = 'SET SESSION time_zone = \'+00:00\', sql_mode = \'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION\';';
-
private IConfig $config;
private IDbConnection $dbConn;
+ private IUsers $users;
private ?TemplateContext $tpl = null;
- public function __construct(IConfig $config) {
+ public function __construct(IConfig $config, IDbConnection $dbConn) {
$this->config = $config;
+ $this->dbConn = $dbConn;
+ $this->users = new DbUsers($dbConn);
}
public function getSiteName(): string {
return $this->config->getValue('site:name', IConfig::T_STR, 'Hanyuu');
}
- public function connectDb(?IDbConnection $dbConn = null): void {
- $dbConn ??= DbTools::create($this->config->getValue('database:dsn', IConfig::T_STR, 'null'));
- $dbConn->execute(self::DB_INIT);
- $this->dbConn = $dbConn;
+ public function getUsers(): IUsers {
+ return $this->users;
}
- public function getDb(): IDbConnection {
+ public function getDatabase(): IDbConnection {
return $this->dbConn;
}
@@ -106,5 +106,26 @@ class HanyuuContext {
$this->router->get('/', function($response, $request) {
return 503;
});
+
+ if(!HAU_DEBUG)
+ return;
+
+ $this->router->get('/test', function() {
+ $users = $this->getUsers();
+ $userInfo = $users->getUserInfoByName('flAsH');
+
+ $ffa = $users->getUserFirstFactorAuthInfos($userInfo);
+ $tfa = $users->getUserSecondFactorAuthInfos($userInfo);
+
+ $pwdInfo = $users->getUserPasswordInfo($userInfo);
+
+ $totpInfo = $users->getUserTOTPInfo($userInfo);
+ $totpGen = $totpInfo->createGenerator();
+
+ $emailInfos = $users->getUserEMailInfos($userInfo);
+
+ $backupInfos = $users->getUserBackupInfos($userInfo);
+
+ });
}
}
diff --git a/src/OTP/IOTPGenerator.php b/src/OTP/IOTPGenerator.php
new file mode 100644
index 0000000..af07b90
--- /dev/null
+++ b/src/OTP/IOTPGenerator.php
@@ -0,0 +1,6 @@
+secretKey))
+ throw new InvalidArgumentException('$secretKey may not be empty');
+ if($this->digits < 1)
+ throw new InvalidArgumentException('$digits must be a positive integer');
+ if($this->interval < 1)
+ throw new InvalidArgumentException('$interval must be a positive integer');
+ if(!in_array($this->hashAlgo, hash_hmac_algos(), true))
+ throw new InvalidArgumentException('$hashAlgo must be a hashing algorithm suitable for hmac');
+ }
+
+ public function getDigits(): int {
+ return $this->digits;
+ }
+
+ public function getInterval(): int {
+ return $this->interval;
+ }
+
+ public function getHashAlgo(): string {
+ return $this->hashAlgo;
+ }
+
+ public function getTimeCode(int $offset = 0, int $timeStamp = -1): int {
+ if($timeStamp < 0)
+ $timeStamp = time();
+
+ // use -1 and 1 to get the previous and next token for user convenience
+ if($offset !== 0)
+ $timeStamp += $this->interval * $offset;
+
+ return (int)(($timeStamp * 1000) / ($this->interval * 1000));
+ }
+
+ public function generate(int $offset = 0, int $timeStamp = -1): string {
+ $timeCode = pack('J', $this->getTimeCode($offset, $timeStamp));
+ $secretKey = Serialiser::base32()->deserialise($this->secretKey);
+ $hash = hash_hmac($this->hashAlgo, $timeCode, $secretKey, true);
+
+ $offset = ord($hash[strlen($hash) - 1]) & 0x0F;
+
+ $bin = (ord($hash[$offset]) & 0x7F) << 24;
+ $bin |= (ord($hash[$offset + 1]) & 0xFF) << 16;
+ $bin |= (ord($hash[$offset + 2]) & 0xFF) << 8;
+ $bin |= ord($hash[$offset + 3]) & 0xFF;
+
+ $otp = (string)($bin % pow(10, $this->digits));
+
+ return str_pad($otp, $this->digits, '0', STR_PAD_LEFT);
+ }
+
+ public static function generateKey(int $bytes = 16): string {
+ if($length < 1)
+ throw new InvalidArgumentException('$bytes must be a positive integer');
+
+ return Serialiser::base32()->serialise(random_bytes($bytes));
+ }
+}
diff --git a/src/Templating/Template.php b/src/Templating/Template.php
index a487c4b..2e1e141 100644
--- a/src/Templating/Template.php
+++ b/src/Templating/Template.php
@@ -3,6 +3,9 @@ namespace Hanyuu\Templating;
use RuntimeException;
+// this entire thing needs to be redone and integrated into Index
+// take this project as an opportunity to do that
+
class Template {
public function __construct(
private TemplateContext $context,
diff --git a/src/Users/Db/DbUserAuthInfo.php b/src/Users/Db/DbUserAuthInfo.php
new file mode 100644
index 0000000..d8269b0
--- /dev/null
+++ b/src/Users/Db/DbUserAuthInfo.php
@@ -0,0 +1,30 @@
+userId = $result->getString(0);
+ $this->type = $result->getString(1);
+ $this->enabled = DateTime::fromUnixTimeSeconds($result->getInteger(2));
+ }
+
+ public function getUserId(): string {
+ return $this->userId;
+ }
+
+ public function getType(): string {
+ return $this->type;
+ }
+
+ public function getEnabledTime(): DateTime {
+ return $this->enabled;
+ }
+}
diff --git a/src/Users/Db/DbUserBackupInfo.php b/src/Users/Db/DbUserBackupInfo.php
new file mode 100644
index 0000000..cf7ee10
--- /dev/null
+++ b/src/Users/Db/DbUserBackupInfo.php
@@ -0,0 +1,42 @@
+userId = $result->getString(0);
+ $this->code = $result->getString(1);
+ $this->created = DateTime::fromUnixTimeSeconds($result->getInteger(2));
+ $this->isUsed = !$result->isNull(3);
+ $this->used = DateTime::fromUnixTimeSeconds($result->isNull(3) ? 0 : $result->getInteger(3));
+ }
+
+ public function getUserId(): string {
+ return $this->userId;
+ }
+
+ public function getCode(): string {
+ return $this->code;
+ }
+
+ public function getCreatedTime(): DateTime {
+ return $this->created;
+ }
+
+ public function isUsed(): bool {
+ return $this->isUsed;
+ }
+
+ public function getUsedTime(): DateTime {
+ return $this->used;
+ }
+}
diff --git a/src/Users/Db/DbUserEMailInfo.php b/src/Users/Db/DbUserEMailInfo.php
new file mode 100644
index 0000000..5a3a500
--- /dev/null
+++ b/src/Users/Db/DbUserEMailInfo.php
@@ -0,0 +1,48 @@
+userId = $result->getString(0);
+ $this->address = $result->getString(1);
+ $this->created = DateTime::fromUnixTimeSeconds($result->getInteger(2));
+ $this->isVerified = !$result->isNull(3);
+ $this->verified = DateTime::fromUnixTimeSeconds($result->isNull(3) ? 0 : $result->getInteger(3));
+ $this->isRecovery = $result->getInteger(4) !== 0;
+ }
+
+ public function getUserId(): string {
+ return $this->userId;
+ }
+
+ public function getAddress(): string {
+ return $this->address;
+ }
+
+ public function getCreatedTime(): DateTime {
+ return $this->created;
+ }
+
+ public function isVerified(): bool {
+ return $this->isVerified;
+ }
+
+ public function getVerifiedTime(): DateTime {
+ return $this->verified;
+ }
+
+ public function isRecovery(): bool {
+ return $this->isRecovery;
+ }
+}
diff --git a/src/Users/Db/DbUserInfo.php b/src/Users/Db/DbUserInfo.php
new file mode 100644
index 0000000..39a0c60
--- /dev/null
+++ b/src/Users/Db/DbUserInfo.php
@@ -0,0 +1,75 @@
+id = $result->getString(0);
+ $this->name = $result->getString(1);
+ $this->country = $result->getString(2);
+ $this->colour = $result->isNull(3) ? Colour::none() : ColourRGB::fromRawRGB($result->getInteger(3));
+ $this->isSuper = $result->getInteger(4) !== 0;
+ $this->timeZone = new TimeZoneInfo($result->getString(5));
+ $this->created = DateTime::fromUnixTimeSeconds($result->getInteger(6));
+ $this->updated = DateTime::fromUnixTimeSeconds($result->getInteger(7));
+ $this->isDeleted = !$result->isNull(8);
+ $this->deleted = DateTime::fromUnixTimeSeconds($result->isNull(8) ? 0 : $result->getInteger(8));
+ }
+
+ public function getId(): string {
+ return $this->id;
+ }
+
+ public function getName(): string {
+ return $this->name;
+ }
+
+ public function getCountryCode(): string {
+ return $this->country;
+ }
+
+ public function getColour(): Colour {
+ return $this->colour;
+ }
+
+ public function isSuper(): bool {
+ return $this->isSuper;
+ }
+
+ public function getTimeZone(): TimeZoneInfo {
+ return $this->timeZone;
+ }
+
+ public function getCreatedTime(): DateTime {
+ return $this->created;
+ }
+
+ public function getUpdatedTime(): DateTime {
+ return $this->updated;
+ }
+
+ public function isDeleted(): bool {
+ return $this->isDeleted;
+ }
+
+ public function getDeletedTime(): DateTime {
+ return $this->deleted;
+ }
+}
diff --git a/src/Users/Db/DbUserPasswordInfo.php b/src/Users/Db/DbUserPasswordInfo.php
new file mode 100644
index 0000000..182f762
--- /dev/null
+++ b/src/Users/Db/DbUserPasswordInfo.php
@@ -0,0 +1,38 @@
+userId = $result->getString(0);
+ $this->hash = $result->getString(1);
+ $this->changed = DateTime::fromUnixTimeSeconds($result->getInteger(2));
+ }
+
+ public function getUserId(): string {
+ return $this->userId;
+ }
+
+ public function getHash(): string {
+ return $this->hash;
+ }
+
+ public function getChangedTime(): DateTime {
+ return $this->changed;
+ }
+
+ public function verifyPassword(string $password): bool {
+ return password_verify($password, $this->hash);
+ }
+
+ public function needsRehash(string|int|null $algo, array $options = []): bool {
+ return password_needs_rehash($this->hash, $algo, $options);
+ }
+}
diff --git a/src/Users/Db/DbUserTOTPInfo.php b/src/Users/Db/DbUserTOTPInfo.php
new file mode 100644
index 0000000..759143b
--- /dev/null
+++ b/src/Users/Db/DbUserTOTPInfo.php
@@ -0,0 +1,35 @@
+userId = $result->getString(0);
+ $this->secretKey = $result->getString(1);
+ $this->changed = DateTime::fromUnixTimeSeconds($result->getInteger(2));
+ }
+
+ public function getUserId(): string {
+ return $this->userId;
+ }
+
+ public function getSecretKey(): string {
+ return $this->secretKey;
+ }
+
+ public function getChangedTime(): DateTime {
+ return $this->changed;
+ }
+
+ public function createGenerator(): TOTPGenerator {
+ return new TOTPGenerator($this->secretKey);
+ }
+}
diff --git a/src/Users/Db/DbUsers.php b/src/Users/Db/DbUsers.php
new file mode 100644
index 0000000..93c7fae
--- /dev/null
+++ b/src/Users/Db/DbUsers.php
@@ -0,0 +1,170 @@
+statements))
+ return $this->statements[$name];
+ return $this->statements[$name] = $this->conn->prepare($query());
+ }
+
+
+ private function fetchUserInfoSingle(IDbResult $result, string $exceptionText): IUserInfo {
+ if(!$result->next())
+ throw new RuntimeException($exceptionText);
+
+ return new DbUserInfo($result);
+ }
+
+ public function getUserInfoById(string $userId): IUserInfo {
+ $stmt = $this->getStatement('get info by id', fn() => ('SELECT ' . implode(',', self::USERS_FIELDS) . ' FROM ' . self::USERS_TABLE . ' WHERE user_id = ?'));
+ $stmt->reset();
+ $stmt->addParameter(1, $userId, DbType::STRING);
+ $stmt->execute();
+
+ return $this->fetchUserInfoSingle($stmt->getResult(), 'no user with $userId found');
+ }
+
+ public function getUserInfoByName(string $userName): IUserInfo {
+ $stmt = $this->getStatement('get info by name', fn() => ('SELECT ' . implode(',', self::USERS_FIELDS) . ' FROM ' . self::USERS_TABLE . ' WHERE user_name = ?'));
+ $stmt->reset();
+ $stmt->addParameter(1, $userName, DbType::STRING);
+ $stmt->execute();
+
+ return $this->fetchUserInfoSingle($stmt->getResult(), 'no user with $userName found');
+ }
+
+
+ private function fetchAuthInfoMultiple(IDbResult $result): array {
+ $array = [];
+
+ while($result->next())
+ $array[] = new DbUserAuthInfo($result);
+
+ return $array;
+ }
+
+ public function getUserFirstFactorAuthInfos(IUserInfo $userInfo): array {
+ $stmt = $this->getStatement('get auth first', fn() => ('SELECT ' . implode(',', self::AUTH_FIELDS) . ' FROM ' . self::AUTH_TABLE . ' WHERE user_id = ? AND user_auth_type IN ("' . implode('", "', self::FIRST_FACTOR_AUTH) . '")'));
+ $stmt->reset();
+ $stmt->addParameter(1, $userInfo->getId(), DbType::STRING);
+ $stmt->execute();
+
+ return $this->fetchAuthInfoMultiple($stmt->getResult());
+ }
+
+ public function getUserSecondFactorAuthInfos(IUserInfo $userInfo): array {
+ $stmt = $this->getStatement('get auth second', fn() => ('SELECT ' . implode(',', self::AUTH_FIELDS) . ' FROM ' . self::AUTH_TABLE . ' WHERE user_id = ? AND user_auth_type IN ("' . implode('", "', self::SECOND_FACTOR_AUTH) . '")'));
+ $stmt->reset();
+ $stmt->addParameter(1, $userInfo->getId(), DbType::STRING);
+ $stmt->execute();
+
+ return $this->fetchAuthInfoMultiple($stmt->getResult());
+ }
+
+
+ public function getUserPasswordInfo(IUserInfo $userInfo): IUserPasswordInfo {
+ $stmt = $this->getStatement('get pwd', fn() => ('SELECT ' . implode(',', self::PASSWORDS_FIELDS) . ' FROM ' . self::PASSWORDS_TABLE . ' WHERE user_id = ?'));
+ $stmt->reset();
+ $stmt->addParameter(1, $userInfo->getId(), DbType::STRING);
+ $stmt->execute();
+ $result = $stmt->getResult();
+
+ if(!$result->next())
+ throw new RuntimeException('no password info for $userInfo found');
+
+ return new DbUserPasswordInfo($result);
+ }
+
+
+ public function getUserTOTPInfo(IUserInfo $userInfo): IUserTOTPInfo {
+ $stmt = $this->getStatement('get totp', fn() => ('SELECT ' . implode(',', self::TOTP_FIELDS) . ' FROM ' . self::TOTP_TABLE . ' WHERE user_id = ?'));
+ $stmt->reset();
+ $stmt->addParameter(1, $userInfo->getId(), DbType::STRING);
+ $stmt->execute();
+ $result = $stmt->getResult();
+
+ if(!$result->next())
+ throw new RuntimeException('no totp info for $userInfo found');
+
+ return new DbUserTOTPInfo($result);
+ }
+
+
+ public function getUserEMailInfos(IUserInfo $userInfo): array {
+ $stmt = $this->getStatement('get emails', fn() => ('SELECT ' . implode(',', self::EMAILS_FIELDS) . ' FROM ' . self::EMAILS_TABLE . ' WHERE user_id = ?'));
+ $stmt->reset();
+ $stmt->addParameter(1, $userInfo->getId(), DbType::STRING);
+ $stmt->execute();
+ $result = $stmt->getResult();
+
+ $array = [];
+
+ while($result->next())
+ $array[] = new DbUserEMailInfo($result);
+
+ return $array;
+ }
+
+
+ public function getUserBackupInfos(IUserInfo $userInfo): array {
+ $stmt = $this->getStatement('get backups', fn() => ('SELECT ' . implode(',', self::BACKUP_FIELDS) . ' FROM ' . self::BACKUP_TABLE . ' WHERE user_id = ?'));
+ $stmt->reset();
+ $stmt->addParameter(1, $userInfo->getId(), DbType::STRING);
+ $stmt->execute();
+ $result = $stmt->getResult();
+
+ $array = [];
+
+ while($result->next())
+ $array[] = new DbUserBackupInfo($result);
+
+ return $array;
+ }
+}
diff --git a/src/Users/IUserAuthInfo.php b/src/Users/IUserAuthInfo.php
new file mode 100644
index 0000000..db3eb2b
--- /dev/null
+++ b/src/Users/IUserAuthInfo.php
@@ -0,0 +1,10 @@
+