Removed existing code.
This commit is contained in:
parent
44bbdc243a
commit
c52c8c00a5
13 changed files with 0 additions and 1019 deletions
|
@ -1,104 +0,0 @@
|
|||
<?php
|
||||
use Index\Data\IDbConnection;
|
||||
use Index\Data\Migration\IDbMigration;
|
||||
|
||||
final class CreateUsersTables_20230109_213225 implements IDbMigration {
|
||||
public function migrate(IDbConnection $conn): void {
|
||||
$conn->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;
|
||||
');
|
||||
}
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
<?php
|
||||
namespace Hanyuu\Auth;
|
||||
|
||||
use Index\XString;
|
||||
use Index\Data\{DbStatementCache,IDbConnection,IDbResult};
|
||||
use Hanyuu\Users\UserInfo;
|
||||
|
||||
class Auth {
|
||||
private const LOGINS_TABLE = 'hau_auth_logins';
|
||||
|
||||
private const LOGINS_FIELDS = [
|
||||
'auth_login_id', 'user_id', 'auth_login_ip', 'auth_login_country',
|
||||
'auth_login_factors_required', 'auth_login_factors_done',
|
||||
'UNIX_TIMESTAMP(auth_login_started)', 'UNIX_TIMESTAMP(auth_login_valid)', 'UNIX_TIMESTAMP(auth_login_completed)',
|
||||
];
|
||||
|
||||
private DbStatementCache $cache;
|
||||
|
||||
public function __construct(
|
||||
private IDbConnection $conn
|
||||
) {
|
||||
$this->cache = new DbStatementCache($conn);
|
||||
}
|
||||
|
||||
|
||||
public function createLoginSession(
|
||||
UserInfo $userInfo,
|
||||
string $remoteAddr,
|
||||
string $countryCode,
|
||||
int $factors
|
||||
): string {
|
||||
$loginId = XString::random(48);
|
||||
|
||||
$stmt = $this->cache->get('INSERT INTO ' . self::LOGINS_TABLE . ' (auth_login_id, user_id, auth_login_ip, auth_login_country, auth_login_factors_required) VALUES (?, ?, INET6_ATON(?), ?, ?)');
|
||||
|
||||
$stmt->addParameter(1, $loginId);
|
||||
$stmt->addParameter(2, $userInfo->getId());
|
||||
$stmt->addParameter(3, $remoteAddr);
|
||||
$stmt->addParameter(4, $countryCode);
|
||||
$stmt->addParameter(5, $factors);
|
||||
$stmt->execute();
|
||||
|
||||
return $loginId;
|
||||
}
|
||||
|
||||
public function destroyLoginSession(AuthLoginInfo $loginInfo): void {
|
||||
$stmt = $this->cache->get('DELETE FROM ' . self::LOGINS_TABLE . ' WHERE auth_login_id = ?');
|
||||
$stmt->addParameter(1, $loginInfo->getId());
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
private function fetchLoginSingle(IDbResult $result, string $exceptionText): UserInfo {
|
||||
if(!$result->next())
|
||||
throw new AuthLoginNotFoundException($exceptionText);
|
||||
|
||||
return new UserInfo($result);
|
||||
}
|
||||
|
||||
public function getLoginSessionById(string $loginId): ?AuthLoginInfo {
|
||||
$stmt = $this->cache->get('SELECT ' . implode(',', self::LOGINS_FIELDS) . ' FROM ' . self::LOGINS_TABLE . ' WHERE auth_login_id = ?');
|
||||
$stmt->addParameter(1, $loginId);
|
||||
$stmt->execute();
|
||||
$result = $stmt->getResult();
|
||||
|
||||
if(!$result->next())
|
||||
return null;
|
||||
|
||||
return new AuthLoginInfo($result);
|
||||
}
|
||||
|
||||
public function incrementLoginSessionDone(AuthLoginInfo $login): void {
|
||||
$stmt = $this->cache->get('UPDATE ' . self::LOGINS_TABLE . ' SET auth_login_factors_done = auth_login_factors_done + 1 WHERE auth_login_id = ?');
|
||||
$stmt->addParameter(1, $login->getId());
|
||||
$stmt->execute();
|
||||
}
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
<?php
|
||||
namespace Hanyuu\Auth;
|
||||
|
||||
use Index\DateTime;
|
||||
use Index\Data\IDbResult;
|
||||
use Index\Net\IPAddress;
|
||||
|
||||
class AuthLoginInfo {
|
||||
private string $id;
|
||||
private string $userId;
|
||||
private IPAddress $remoteAddr;
|
||||
private string $country;
|
||||
private int $factorsRequired;
|
||||
private int $factorsDone;
|
||||
private DateTime $started;
|
||||
private DateTime $valid;
|
||||
private bool $hasCompleted;
|
||||
private DateTime $completed;
|
||||
|
||||
public function __construct(IDbResult $result) {
|
||||
$this->id = $result->getString(0);
|
||||
$this->userId = $result->getString(1);
|
||||
$this->remoteAddr = new IPAddress($result->getString(2));
|
||||
$this->country = $result->getString(3);
|
||||
$this->factorsRequired = $result->getInteger(4);
|
||||
$this->factorsDone = $result->getInteger(5);
|
||||
$this->started = DateTime::fromUnixTimeSeconds($result->getInteger(6));
|
||||
$this->valid = DateTime::fromUnixTimeSeconds($result->getInteger(7));
|
||||
$this->hasCompleted = !$result->isNull(8);
|
||||
$this->completed = DateTime::fromUnixTimeSeconds($result->getInteger(8));
|
||||
}
|
||||
|
||||
public function getId(): string {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getUserId(): string {
|
||||
return $this->userId;
|
||||
}
|
||||
|
||||
public function getRemoteAddress(): IPAddress {
|
||||
return $this->remoteAddr;
|
||||
}
|
||||
|
||||
public function getCountryCode(): string {
|
||||
return $this->country;
|
||||
}
|
||||
|
||||
public function getFactorsRequired(): int {
|
||||
return $this->factorsRequired;
|
||||
}
|
||||
|
||||
public function getFactorsDone(): int {
|
||||
return $this->factorsDone;
|
||||
}
|
||||
|
||||
public function getStartedTime(): DateTime {
|
||||
return $this->started;
|
||||
}
|
||||
|
||||
public function isValid(): bool {
|
||||
return DateTime::utcNow()->isLessThan($this->valid);
|
||||
}
|
||||
|
||||
public function getValidTime(): DateTime {
|
||||
return $this->valid;
|
||||
}
|
||||
|
||||
public function hasCompleted(): bool {
|
||||
return $this->hasCompleted;
|
||||
}
|
||||
|
||||
public function getCompletedTime(): DateTime {
|
||||
return $this->completed;
|
||||
}
|
||||
}
|
|
@ -1,269 +0,0 @@
|
|||
<?php
|
||||
namespace Hanyuu\Auth;
|
||||
|
||||
use RuntimeException;
|
||||
use Index\Http\Routing\{HttpGet,HttpMiddleware,HttpPost,RouteHandler};
|
||||
use Syokuhou\IConfig;
|
||||
use Hanyuu\HanyuuContext;
|
||||
use Hanyuu\Auth\{Auth,AuthLoginInfo};
|
||||
use Hanyuu\Users\Users;
|
||||
|
||||
// VERY IMPORTANT TODO: CSRF AND RATE LIMITING
|
||||
|
||||
class AuthRoutes extends RouteHandler {
|
||||
private HanyuuContext $context;
|
||||
private Users $users;
|
||||
private IConfig $config;
|
||||
private Auth $auth;
|
||||
|
||||
private ?AuthLoginInfo $loginSession;
|
||||
|
||||
public function __construct(
|
||||
HanyuuContext $ctx,
|
||||
IConfig $config
|
||||
) {
|
||||
$this->context = $ctx;
|
||||
$this->config = $config;
|
||||
$this->users = $ctx->getUsers();
|
||||
|
||||
$this->context->setUpAuth();
|
||||
$this->auth = $ctx->getAuth();
|
||||
}
|
||||
|
||||
private function getLoginCookieName(): string {
|
||||
return $this->config->getString('login_cookie', 'hau_login');
|
||||
}
|
||||
|
||||
private function destroyLoginSession($response): void {
|
||||
if($this->loginSession !== null) {
|
||||
$response->removeCookie($this->getLoginCookieName());
|
||||
$this->auth->destroyLoginSession($this->loginSession);
|
||||
$this->loginSession = null;
|
||||
}
|
||||
}
|
||||
|
||||
private function ensureLoginIncomplete($response): bool {
|
||||
$loginSession = $this->loginSession;
|
||||
if($loginSession === null) {
|
||||
$response->redirect('/login');
|
||||
return false;
|
||||
}
|
||||
|
||||
if($loginSession->getFactorsDone() >= $loginSession->getFactorsRequired()) {
|
||||
$response->redirect('/login/done');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#[HttpMiddleware('/login')]
|
||||
public function filterLogin($response, $request) {
|
||||
$loginId = (string)$request->getCookie($this->getLoginCookieName());
|
||||
$this->loginSession = empty($loginId) ? null : $this->auth->getLoginSessionById($loginId);
|
||||
|
||||
if($this->loginSession !== null) {
|
||||
if(!$this->loginSession->isValid()
|
||||
|| $this->loginSession->getRemoteAddress()->getCleanAddress() !== $_SERVER['REMOTE_ADDR'])
|
||||
$this->destroyLoginSession($response);
|
||||
}
|
||||
}
|
||||
|
||||
#[HttpGet('/login')]
|
||||
public function getLogin($response, $request) {
|
||||
$this->destroyLoginSession($response);
|
||||
|
||||
$userName = (string)$request->getParam('username');
|
||||
$errorId = (string)$request->getParam('error');
|
||||
$errorText = [
|
||||
'invalid_username' => 'Either no username was provided or it was incorrectly formatted.',
|
||||
'wrong_password' => 'The provided password was incorrect.',
|
||||
'user_not_found' => 'No user with this name exists. NOTE: point to registration somehow',
|
||||
'user_deleted' => 'Your account has been marked for deletion. Contact staff to revert this.',
|
||||
][$errorId] ?? '';
|
||||
|
||||
return $this->context->renderTemplate('auth/login', [
|
||||
'user_name' => $userName,
|
||||
'error_name' => $errorId,
|
||||
'error_text' => $errorText,
|
||||
]);
|
||||
}
|
||||
|
||||
#[HttpPost('/login')]
|
||||
public function postLogin($response, $request) {
|
||||
if(!$request->isFormContent())
|
||||
return 400;
|
||||
$form = $request->getContent();
|
||||
|
||||
// should be adjusted to respond with json to ajax shit
|
||||
|
||||
$this->destroyLoginSession($response);
|
||||
|
||||
$userName = (string)$form->getParam('username');
|
||||
$password = (string)$form->getParam('password');
|
||||
|
||||
if(empty($userName)) {
|
||||
$response->redirect('/login?error=invalid_username');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$userInfo = $this->users->getUserInfoByName($userName);
|
||||
} catch(RuntimeException $ex) {
|
||||
$response->redirect('/login?error=user_not_found&username=' . rawurlencode($userName));
|
||||
return;
|
||||
}
|
||||
|
||||
if($userInfo->isDeleted()) {
|
||||
$response->redirect('/login?error=user_deleted');
|
||||
return;
|
||||
}
|
||||
|
||||
$passwordInfo = $this->users->getUserPasswordInfo($userInfo);
|
||||
if(!$passwordInfo->verifyPassword($password)) {
|
||||
$response->redirect('/login?error=wrong_password');
|
||||
return;
|
||||
}
|
||||
|
||||
$factorsRequired = $this->users->countUserTFAMethods($userInfo) > 0 ? 2 : 1;
|
||||
$loginId = $this->auth->createLoginSession($userInfo, $_SERVER['REMOTE_ADDR'], 'A1', $factorsRequired);
|
||||
$this->loginSession = $this->auth->getLoginSessionById($loginId);
|
||||
|
||||
$this->auth->incrementLoginSessionDone($this->loginSession);
|
||||
|
||||
// check for rehash
|
||||
|
||||
$response->addCookie($this->getLoginCookieName(), $loginId, strtotime('+30 minutes'), '/', '', true, true, true);
|
||||
|
||||
$response->redirect($factorsRequired > 1 ? '/login/tfa' : '/login/done');
|
||||
}
|
||||
|
||||
#[HttpGet('/login/tfa')]
|
||||
public function getLoginTFA($response, $request) {
|
||||
if(!$this->ensureLoginIncomplete($response))
|
||||
return;
|
||||
|
||||
$errorId = (string)$request->getParam('error');
|
||||
$errorText = [
|
||||
'invalid_method' => 'Either no method was provided or it was incorrectly formatted.',
|
||||
][$errorId] ?? '';
|
||||
|
||||
$userInfo = $this->users->getUserInfoById($this->loginSession->getUserId());
|
||||
$authMethods = $this->users->getUserTFAMethods($userInfo);
|
||||
$authMethodNames = [];
|
||||
foreach($authMethods as $authMethod)
|
||||
$authMethodNames[] = $authMethod->getType();
|
||||
|
||||
if(count($authMethods) === 1) {
|
||||
$response->redirect("/login/tfa/{$authMethods[0]->getType()}");
|
||||
return;
|
||||
}
|
||||
|
||||
return $this->context->renderTemplate('auth/login-tfa', [
|
||||
'user_info' => $userInfo,
|
||||
'auth_methods' => $authMethods,
|
||||
'auth_method_names' => $authMethodNames,
|
||||
'login_session' => $this->loginSession,
|
||||
]);
|
||||
}
|
||||
|
||||
#[HttpGet('/login/done')]
|
||||
public function getLoginDone($response, $request) {
|
||||
if($this->loginSession === null) {
|
||||
$response->redirect('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
if($this->loginSession->getFactorsDone() < $this->loginSession->getFactorsRequired()) {
|
||||
$response->redirect('/login/pick');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->destroyLoginSession($response);
|
||||
|
||||
return "you've successfully authenticated but there's no session system yet lol";
|
||||
}
|
||||
|
||||
#[HttpGet('/login/tfa/totp')]
|
||||
public function getLoginTOTP($response, $request) {
|
||||
if(!$this->ensureLoginIncomplete($response))
|
||||
return;
|
||||
|
||||
$userInfo = $this->users->getUserInfoById($this->loginSession->getUserId());
|
||||
|
||||
$errorId = (string)$request->getParam('error');
|
||||
$errorText = [
|
||||
'invalid_method' => 'Either no method was provided or it was incorrectly formatted.',
|
||||
][$errorId] ?? '';
|
||||
|
||||
return $this->context->renderTemplate('auth/login-totp', [
|
||||
'user_info' => $userInfo,
|
||||
'login_session' => $this->loginSession,
|
||||
]);
|
||||
}
|
||||
|
||||
#[HttpPost('/login/tfa/totp')]
|
||||
public function postLoginTOTP($response, $request) {
|
||||
if(!$request->isFormContent())
|
||||
return 400;
|
||||
$form = $request->getContent();
|
||||
|
||||
if(!$this->ensureLoginIncomplete($response))
|
||||
return;
|
||||
|
||||
$userInfo = $this->users->getUserInfoById($this->loginSession->getUserId());
|
||||
|
||||
$totpInfo = $this->users->getUserTOTPInfo($userInfo);
|
||||
$totpValid = $totpInfo->generateValidCodes();
|
||||
$totpCode = (string)$form->getParam('code');
|
||||
|
||||
if(!in_array($totpCode, $totpValid, true)) {
|
||||
$response->redirect('/login/tfa/totp?error=wrong_totp');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->auth->incrementLoginSessionDone($this->loginSession);
|
||||
|
||||
$response->redirect('/login/done');
|
||||
}
|
||||
|
||||
#[HttpGet('/login/tfa/u2f')]
|
||||
public function getLoginU2F($response, $request) {
|
||||
return 503;
|
||||
}
|
||||
|
||||
#[HttpPost('/login/tfa/u2f')]
|
||||
public function postLoginU2F($response, $request) {
|
||||
return 503;
|
||||
}
|
||||
|
||||
#[HttpGet('/login/tfa/backup')]
|
||||
public function getLoginBackup($response, $request) {
|
||||
return 503;
|
||||
}
|
||||
|
||||
#[HttpPost('/login/tfa/backup')]
|
||||
public function postLoginBackup($response, $request) {
|
||||
return 503;
|
||||
}
|
||||
|
||||
#[HttpGet('/register')]
|
||||
public function getRegister($response, $request) {
|
||||
return 503;
|
||||
}
|
||||
|
||||
#[HttpGet('/forgot-username')]
|
||||
public function getForgotUserName($response, $request) {
|
||||
return 503;
|
||||
}
|
||||
|
||||
#[HttpGet('/forgot-password')]
|
||||
public function getForgotPassword($response, $request) {
|
||||
return 503;
|
||||
}
|
||||
|
||||
#[HttpGet('/recover-password')]
|
||||
public function getRecoverPassword($response, $request) {
|
||||
return 503;
|
||||
}
|
||||
}
|
|
@ -6,29 +6,20 @@ use Index\Data\IDbConnection;
|
|||
use Index\Data\Migration\{IDbMigrationRepo,DbMigrationManager,FsDbMigrationRepo};
|
||||
use Sasae\SasaeEnvironment;
|
||||
use Syokuhou\IConfig;
|
||||
use Hanyuu\Auth\{Auth,AuthRoutes};
|
||||
use Hanyuu\Users\Users;
|
||||
|
||||
class HanyuuContext {
|
||||
private IConfig $config;
|
||||
private IDbConnection $dbConn;
|
||||
private Users $users;
|
||||
private Auth $auth;
|
||||
private ?SasaeEnvironment $templating = null;
|
||||
private SiteInfo $siteInfo;
|
||||
|
||||
public function __construct(IConfig $config, IDbConnection $dbConn) {
|
||||
$this->config = $config;
|
||||
$this->dbConn = $dbConn;
|
||||
$this->users = new Users($dbConn);
|
||||
|
||||
$this->siteInfo = new SiteInfo($config->scopeTo('site'));
|
||||
}
|
||||
|
||||
public function getUsers(): Users {
|
||||
return $this->users;
|
||||
}
|
||||
|
||||
public function getDatabase(): IDbConnection {
|
||||
return $this->dbConn;
|
||||
}
|
||||
|
@ -69,14 +60,6 @@ class HanyuuContext {
|
|||
return $this->getTemplating()->render(...$args);
|
||||
}
|
||||
|
||||
public function setUpAuth(): void {
|
||||
$this->auth = new Auth($this->dbConn);
|
||||
}
|
||||
|
||||
public function getAuth(): Auth {
|
||||
return $this->auth;
|
||||
}
|
||||
|
||||
public function createRouting(): RoutingContext {
|
||||
$routingCtx = new RoutingContext($this->getTemplating());
|
||||
|
||||
|
@ -84,8 +67,6 @@ class HanyuuContext {
|
|||
return 503;
|
||||
});
|
||||
|
||||
$routingCtx->register(new AuthRoutes($this, $this->config->scopeTo('auth')));
|
||||
|
||||
return $routingCtx;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
<?php
|
||||
namespace Hanyuu;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Index\Serialisation\Base32;
|
||||
|
||||
class TOTPGenerator {
|
||||
public const DIGITS = 6;
|
||||
public const INTERVAL = 30000;
|
||||
|
||||
public function __construct(private string $secretKey) {}
|
||||
|
||||
public static function generateKey(): string {
|
||||
return Base32::encode(random_bytes(16));
|
||||
}
|
||||
|
||||
public static function timecode(?int $timestamp = null): int {
|
||||
$timestamp ??= time();
|
||||
return (int)(($timestamp * 1000) / self::INTERVAL);
|
||||
}
|
||||
|
||||
public function generate(?int $timecode = null): string {
|
||||
$timecode ??= self::timecode();
|
||||
|
||||
$hash = hash_hmac('sha1', pack('J', $timecode), Base32::decode($this->secretKey), true);
|
||||
$offset = ord($hash[strlen($hash) - 1]) & 0x0F;
|
||||
|
||||
$bin = 0;
|
||||
$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 = $bin % pow(10, self::DIGITS);
|
||||
|
||||
return str_pad((string)$otp, self::DIGITS, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
public function generateRange(int $range = 1, ?int $timecode = null): array {
|
||||
if($range < 1)
|
||||
throw new InvalidArgumentException('$range must be greater than 0.');
|
||||
|
||||
$timecode ??= self::timecode();
|
||||
$tokens = [$this->generate($timecode)];
|
||||
|
||||
for($i = 1; $i <= $range; ++$i)
|
||||
$tokens[] = $this->generate($timecode - $i);
|
||||
for($i = 1; $i <= $range; ++$i)
|
||||
$tokens[] = $this->generate($timecode + $i);
|
||||
|
||||
return $tokens;
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
<?php
|
||||
namespace Hanyuu\Users;
|
||||
|
||||
use Index\DateTime;
|
||||
use Index\Data\IDbResult;
|
||||
|
||||
class UserAuthInfo {
|
||||
private string $userId;
|
||||
private string $type;
|
||||
private DateTime $enabled;
|
||||
|
||||
public function __construct(IDbResult $result) {
|
||||
$this->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;
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
<?php
|
||||
namespace Hanyuu\Users;
|
||||
|
||||
use Index\DateTime;
|
||||
use Index\Data\IDbResult;
|
||||
|
||||
class UserBackupInfo {
|
||||
private string $userId;
|
||||
private string $code;
|
||||
private DateTime $created;
|
||||
private bool $isUsed;
|
||||
private DateTime $used;
|
||||
|
||||
public function __construct(IDbResult $result) {
|
||||
$this->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;
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
<?php
|
||||
namespace Hanyuu\Users;
|
||||
|
||||
use Index\DateTime;
|
||||
use Index\Data\IDbResult;
|
||||
|
||||
class UserEMailInfo {
|
||||
private string $userId;
|
||||
private string $address;
|
||||
private DateTime $created;
|
||||
private bool $isVerified;
|
||||
private DateTime $verified;
|
||||
private bool $isRecovery;
|
||||
|
||||
public function __construct(IDbResult $result) {
|
||||
$this->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;
|
||||
}
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
<?php
|
||||
namespace Hanyuu\Users;
|
||||
|
||||
use Index\DateTime;
|
||||
use Index\TimeZoneInfo;
|
||||
use Index\Colour\Colour;
|
||||
use Index\Colour\ColourRGB;
|
||||
use Index\Data\IDbResult;
|
||||
|
||||
class UserInfo {
|
||||
private string $id;
|
||||
private string $name;
|
||||
private string $country;
|
||||
private Colour $colour;
|
||||
private bool $isSuper;
|
||||
private TimeZoneInfo $timeZone;
|
||||
private DateTime $created;
|
||||
private DateTime $updated;
|
||||
private bool $isDeleted;
|
||||
private DateTime $deleted;
|
||||
|
||||
public function __construct(IDbResult $result) {
|
||||
$this->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;
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
<?php
|
||||
namespace Hanyuu\Users;
|
||||
|
||||
use Index\DateTime;
|
||||
use Index\Data\IDbResult;
|
||||
|
||||
class UserPasswordInfo {
|
||||
private string $userId;
|
||||
private string $hash;
|
||||
private DateTime $changed;
|
||||
|
||||
public function __construct(IDbResult $result) {
|
||||
$this->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);
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
<?php
|
||||
namespace Hanyuu\Users;
|
||||
|
||||
use Index\DateTime;
|
||||
use Index\Data\IDbResult;
|
||||
use Hanyuu\TOTPGenerator;
|
||||
|
||||
class UserTOTPInfo {
|
||||
private string $userId;
|
||||
private string $secretKey;
|
||||
private DateTime $changed;
|
||||
|
||||
public function __construct(IDbResult $result) {
|
||||
$this->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);
|
||||
}
|
||||
|
||||
public function generateValidCodes(int $range = 1, ?int $timecode = null): array {
|
||||
return $this->createGenerator()->generateRange($range, $timecode);
|
||||
}
|
||||
}
|
|
@ -1,157 +0,0 @@
|
|||
<?php
|
||||
namespace Hanyuu\Users;
|
||||
|
||||
use RuntimeException;
|
||||
use Index\Data\{DbStatementCache,IDbConnection,IDbStatement,IDbResult};
|
||||
use Hanyuu\Auth\{Auth,IAuthMethod};
|
||||
use Hanyuu\Users\{UserInfo,UserPasswordInfo,UserTOTPInfo};
|
||||
|
||||
class Users {
|
||||
private const USERS_TABLE = 'hau_users';
|
||||
private const TFA_TABLE = 'hau_users_tfa';
|
||||
private const BACKUP_TABLE = 'hau_users_backup';
|
||||
private const EMAILS_TABLE = 'hau_users_emails';
|
||||
private const PASSWORDS_TABLE = 'hau_users_passwords';
|
||||
private const TOTP_TABLE = 'hau_users_totp';
|
||||
|
||||
private const USERS_FIELDS = [
|
||||
'user_id', 'user_name', 'user_country', 'user_colour', 'user_super', 'user_time_zone',
|
||||
'UNIX_TIMESTAMP(user_created)', 'UNIX_TIMESTAMP(user_updated)', 'UNIX_TIMESTAMP(user_deleted)',
|
||||
];
|
||||
private const TFA_FIELDS = [
|
||||
'user_id', 'user_tfa_type', 'UNIX_TIMESTAMP(user_tfa_enabled)',
|
||||
];
|
||||
private const PASSWORDS_FIELDS = [
|
||||
'user_id', 'user_password_hash', 'UNIX_TIMESTAMP(user_password_changed)',
|
||||
];
|
||||
private const TOTP_FIELDS = [
|
||||
'user_id', 'user_totp_key', 'UNIX_TIMESTAMP(user_totp_changed)',
|
||||
];
|
||||
private const EMAILS_FIELDS = [
|
||||
'user_id', 'user_email_address', 'UNIX_TIMESTAMP(user_email_created)', 'UNIX_TIMESTAMP(user_email_verified)', 'user_email_recovery',
|
||||
];
|
||||
private const BACKUP_FIELDS = [
|
||||
'user_id', 'user_backup_code', 'UNIX_TIMESTAMP(user_backup_created)', 'UNIX_TIMESTAMP(user_backup_used)',
|
||||
];
|
||||
|
||||
private DbStatementCache $cache;
|
||||
|
||||
public function __construct(
|
||||
private IDbConnection $conn
|
||||
) {
|
||||
$this->cache = new DbStatementCache($conn);
|
||||
}
|
||||
|
||||
private function fetchUserInfoSingle(IDbResult $result, string $exceptionText): UserInfo {
|
||||
if(!$result->next())
|
||||
throw new RuntimeException($exceptionText);
|
||||
|
||||
return new UserInfo($result);
|
||||
}
|
||||
|
||||
public function getUserInfoById(string $userId): UserInfo {
|
||||
$stmt = $this->cache->get('SELECT ' . implode(',', self::USERS_FIELDS) . ' FROM ' . self::USERS_TABLE . ' WHERE user_id = ?');
|
||||
$stmt->addParameter(1, $userId);
|
||||
$stmt->execute();
|
||||
|
||||
return $this->fetchUserInfoSingle($stmt->getResult(), 'no user with $userId found');
|
||||
}
|
||||
|
||||
public function getUserInfoByName(string $userName): UserInfo {
|
||||
$stmt = $this->cache->get('SELECT ' . implode(',', self::USERS_FIELDS) . ' FROM ' . self::USERS_TABLE . ' WHERE user_name = ?');
|
||||
$stmt->addParameter(1, $userName);
|
||||
$stmt->execute();
|
||||
|
||||
return $this->fetchUserInfoSingle($stmt->getResult(), 'no user with $userName found');
|
||||
}
|
||||
|
||||
|
||||
public function countUserTFAMethods(UserInfo $userInfo): int {
|
||||
$stmt = $this->cache->get('SELECT COUNT(*) FROM ' . self::TFA_TABLE . ' WHERE user_id = ?');
|
||||
$stmt->addParameter(1, $userInfo->getId());
|
||||
$stmt->execute();
|
||||
$result = $stmt->getResult();
|
||||
|
||||
return $result->next() ? $result->getInteger(0) : 0;
|
||||
}
|
||||
|
||||
public function getUserTFAMethods(UserInfo $userInfo): array {
|
||||
$stmt = $this->cache->get('SELECT ' . implode(',', self::TFA_FIELDS) . ' FROM ' . self::TFA_TABLE . ' WHERE user_id = ?');
|
||||
$stmt->addParameter(1, $userInfo->getId());
|
||||
$stmt->execute();
|
||||
|
||||
$result = $stmt->getResult();
|
||||
$array = [];
|
||||
|
||||
while($result->next())
|
||||
$array[] = new UserAuthInfo($result);
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
public function checkUserTFAMethod(UserInfo $userInfo, IAuthMethod $method): bool {
|
||||
$stmt = $this->cache->get('SELECT COUNT(*) FROM ' . self::TFA_TABLE . ' WHERE user_id = ? AND user_tfa_type = ?');
|
||||
$stmt->addParameter(1, $userInfo->getId());
|
||||
$stmt->addParameter(2, $method->getName());
|
||||
$stmt->execute();
|
||||
$result = $stmt->getResult();
|
||||
|
||||
return $result->next() ? $result->getInteger(0) !== 0 : false;
|
||||
}
|
||||
|
||||
|
||||
public function getUserPasswordInfo(UserInfo $userInfo): UserPasswordInfo {
|
||||
$stmt = $this->cache->get('SELECT ' . implode(',', self::PASSWORDS_FIELDS) . ' FROM ' . self::PASSWORDS_TABLE . ' WHERE user_id = ?');
|
||||
$stmt->addParameter(1, $userInfo->getId());
|
||||
$stmt->execute();
|
||||
$result = $stmt->getResult();
|
||||
|
||||
if(!$result->next())
|
||||
throw new RuntimeException('no password info for $userInfo found');
|
||||
|
||||
return new UserPasswordInfo($result);
|
||||
}
|
||||
|
||||
|
||||
public function getUserTOTPInfo(UserInfo $userInfo): UserTOTPInfo {
|
||||
$stmt = $this->cache->get('SELECT ' . implode(',', self::TOTP_FIELDS) . ' FROM ' . self::TOTP_TABLE . ' WHERE user_id = ?');
|
||||
$stmt->addParameter(1, $userInfo->getId());
|
||||
$stmt->execute();
|
||||
$result = $stmt->getResult();
|
||||
|
||||
if(!$result->next())
|
||||
throw new RuntimeException('no totp info for $userInfo found');
|
||||
|
||||
return new UserTOTPInfo($result);
|
||||
}
|
||||
|
||||
|
||||
public function getUserEMailInfos(UserInfo $userInfo): array {
|
||||
$stmt = $this->cache->get('SELECT ' . implode(',', self::EMAILS_FIELDS) . ' FROM ' . self::EMAILS_TABLE . ' WHERE user_id = ?');
|
||||
$stmt->addParameter(1, $userInfo->getId());
|
||||
$stmt->execute();
|
||||
$result = $stmt->getResult();
|
||||
|
||||
$array = [];
|
||||
|
||||
while($result->next())
|
||||
$array[] = new UserEMailInfo($result);
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
|
||||
public function getUserBackupInfos(UserInfo $userInfo): array {
|
||||
$stmt = $this->cache->get('SELECT ' . implode(',', self::BACKUP_FIELDS) . ' FROM ' . self::BACKUP_TABLE . ' WHERE user_id = ?');
|
||||
$stmt->addParameter(1, $userInfo->getId());
|
||||
$stmt->execute();
|
||||
$result = $stmt->getResult();
|
||||
|
||||
$array = [];
|
||||
|
||||
while($result->next())
|
||||
$array[] = new UserBackupInfo($result);
|
||||
|
||||
return $array;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue