Backlogged things.

This commit is contained in:
flash 2023-10-18 10:34:30 +00:00
parent d9e175699e
commit caeb4af8ef
22 changed files with 877 additions and 63 deletions

@ -1 +1 @@
Subproject commit fbe4fe18decd502a0ca15ffe8a7c3b2d847349d5
Subproject commit bce5ba77a268ecd6338d0e3520e41ff4c40cbeda

163
public/assets/hanyuu.css Normal file
View file

@ -0,0 +1,163 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
position: relative;
}
html, body {
width: 100%;
height: 100%;
}
body {
background-color: #111;
color: #fff;
font: 12px/20px Verdana, Geneva, Arial, Helvetica, sans-serif;
}
@media (prefers-color-scheme: light) {
body {
background-color: #ddd;
color: #000;
}
}
[hidden],
.hidden {
display: none !important;
}
.http-err {
padding: 40px 60px;
}
.http-err-title {
font-size: 3em;
line-height: 1.5em;
font-weight: 700;
}
.http-err-paragraph {
font-size: 1.2em;
line-height: 1.5em;
}
.auth {
margin: 0 auto;
padding: 10px;
padding-top: 10vh;
box-sizing: content-box;
}
.auth-header {
font-size: 5em;
line-height: 1.5em;
color: #a77bca;
text-shadow: 0 1px 5px #a77bca;
text-align: center;
padding: 10px 0;
}
.auth-avatar {
text-align: center;
margin: 10px 0;
}
.auth-avatar-image {
width: 100px;
height: 100px;
overflow: hidden;
border-radius: 5%;
display: inline-block;
}
.auth-avatar img {
vertical-align: middle;
width: 100%;
height: 100%;
}
.auth-login {
max-width: 400px;
width: 100%;
margin: 0 auto;
}
.auth-fields {
max-width: 300px;
width: 100%;
margin: 0 auto;
margin-bottom: 10px;
}
.auth-input {
display: block;
width: 100%;
border-left: 5px solid #aaa;
margin: 10px 0;
padding: 2px 7px;
padding-right: 12px;
transition: border-color .2s;
}
.auth-input:focus-within {
border-color: #a77bca;
}
.auth-input-title {
display: block;
font-size: 1.2em;
line-height: 1.5em;
}
.auth-input-value {
display: block;
}
.auth-input-value input {
display: block;
width: 100%;
border-width: 0;
background-color: inherit;
outline-style: none !important;
font-size: 1.2em;
line-height: 1.5em;
font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;
}
.auth-buttons {
max-width: 200px;
width: 100%;
margin: 0 auto;
}
.auth-button {
display: block;
width: 100%;
font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;
font-size: 12px;
line-height: 1.5em;
background-color: #bbb;
border-width: 0;
border-radius: 5px;
margin: 5px 0;
padding: 5px;
color: #222;
text-align: center;
text-decoration: none;
cursor: default;
transition: background-color .2s;
}
.auth-button:hover,
.auth-button:focus {
background-color: #ccc;
}
.auth-button:active {
background-color: #aaa;
}
.auth-button-primary {
font-size: 1.3em;
background-color: #a77bca;
color: #fff;
}
.auth-button-primary:hover,
.auth-button-primary:focus {
background-color: #b28bd1;
}
.auth-button-primary:active {
background-color: #925bbd;
}

0
public/assets/hanyuu.js Normal file
View file

7
src/Auth/Auth.php Normal file
View file

@ -0,0 +1,7 @@
<?php
namespace Hanyuu\Auth;
use InvalidArgumentException;
use RuntimeException;
abstract class Auth {}

281
src/Auth/AuthRoutes.php Normal file
View file

@ -0,0 +1,281 @@
<?php
namespace Hanyuu\Auth;
use Index\Routing\IRouter;
use Hanyuu\HanyuuContext;
use Hanyuu\Auth\Auth;
use Hanyuu\Auth\IAuthLogin;
use Hanyuu\Config\IConfig;
use Hanyuu\Users\IUserInfo;
use Hanyuu\Users\IUsers;
use Hanyuu\Users\UserNotFoundException;
// VERY IMPORTANT TODO: CSRF AND RATE LIMITING
class AuthRoutes {
private HanyuuContext $context;
private IUsers $users;
private IConfig $config;
private Auth $auth;
private ?IAuthLogin $loginSession;
public function __construct(
IRouter $router,
HanyuuContext $ctx,
IConfig $config
) {
$this->context = $ctx;
$this->config = $config;
$this->users = $ctx->getUsers();
$this->context->setUpAuth();
$this->auth = $ctx->getAuth();
$router->use('/login', [$this, 'filterLogin']);
$router->get('/login', [$this, 'getLogin']);
$router->post('/login', [$this, 'postLogin']);
$router->get('/login/tfa', [$this, 'getLoginTFA']);
$router->get('/login/done', [$this, 'getLoginDone']);
$router->get('/login/tfa/totp', [$this, 'getLoginTOTP']);
$router->post('/login/tfa/totp', [$this, 'postLoginTOTP']);
$router->get('/login/tfa/u2f', [$this, 'getLoginU2F']);
$router->post('/login/tfa/u2f', [$this, 'postLoginU2F']);
$router->get('/login/tfa/backup', [$this, 'getLoginBackup']);
$router->post('/login/tfa/backup', [$this, 'postLoginBackup']);
$router->get('/register', [$this, 'getRegister']);
$router->get('/forgot-username', [$this, 'getForgotUserName']);
$router->get('/forgot-password', [$this, 'getForgotPassword']);
$router->get('/recover-password', [$this, 'getRecoverPassword']);
}
private function getLoginCookieName(): string {
return $this->config->getValue('login_cookie', IConfig::T_STR, '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;
}
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);
}
}
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', [
'userName' => $userName,
'errorId' => $errorId,
'errorText' => $errorText,
]);
}
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(UserNotFoundException $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');
}
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', [
'userInfo' => $userInfo,
'authMethods' => $authMethods,
'authMethodNames' => $authMethodNames,
'loginSession' => $this->loginSession,
]);
}
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";
}
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', [
'userInfo' => $userInfo,
'loginSession' => $this->loginSession,
]);
}
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');
}
public function getLoginU2F($response, $request) {
return 503;
}
public function postLoginU2F($response, $request) {
return 503;
}
public function getLoginBackup($response, $request) {
return 503;
}
public function postLoginBackup($response, $request) {
return 503;
}
public function getRegister($response, $request) {
return 503;
}
public function getForgotUserName($response, $request) {
return 503;
}
public function getForgotPassword($response, $request) {
return 503;
}
public function getRecoverPassword($response, $request) {
return 503;
}
}

86
src/Auth/Db/DbAuth.php Normal file
View file

@ -0,0 +1,86 @@
<?php
namespace Hanyuu\Auth\Db;
use Index\XString;
use Index\Data\IDbConnection;
use Index\Data\IDbResult;
use Index\Data\DbType;
use Hanyuu\StatementCache;
use Hanyuu\Auth\IAuthLogin;
use Hanyuu\Auth\IAuthMethod;
use Hanyuu\Auth\Auth;
use Hanyuu\Users\IUserInfo;
class DbAuth extends 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 StatementCache $stmts;
public function __construct(
private IDbConnection $conn
) {
$this->stmts = new StatementCache($conn);
}
public function createLoginSession(
IUserInfo $userInfo,
string $remoteAddr,
string $countryCode,
int $factors
): string {
$loginId = XString::random(48);
$stmt = $this->stmts->getStatement('create login', function() {
return '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, DbType::STRING);
$stmt->addParameter(2, $userInfo->getId(), DbType::STRING);
$stmt->addParameter(3, $remoteAddr, DbType::STRING);
$stmt->addParameter(4, $countryCode, DbType::STRING);
$stmt->addParameter(5, $factors, DbType::INTEGER);
$stmt->execute();
return $loginId;
}
public function destroyLoginSession(IAuthLogin $loginInfo): void {
$stmt = $this->stmts->getStatement('destroy login', fn() => ('DELETE FROM ' . self::LOGINS_TABLE . ' WHERE auth_login_id = ?'));
$stmt->addParameter(1, $loginInfo->getId(), DbType::STRING);
$stmt->execute();
}
private function fetchLoginSingle(IDbResult $result, string $exceptionText): IUserInfo {
if(!$result->next())
throw new AuthLoginNotFoundException($exceptionText);
return new DbUserInfo($result);
}
public function getLoginSessionById(string $loginId): ?IAuthLogin {
$stmt = $this->stmts->getStatement('get login by id', fn() => ('SELECT ' . implode(',', self::LOGINS_FIELDS) . ' FROM ' . self::LOGINS_TABLE . ' WHERE auth_login_id = ?'));
$stmt->addParameter(1, $loginId, DbType::STRING);
$stmt->execute();
$result = $stmt->getResult();
if(!$result->next())
return null;
return new DbAuthLogin($result);
}
public function incrementLoginSessionDone(IAuthLogin $login): void {
$stmt = $this->stmts->getStatement('increment login done', fn() => ('UPDATE ' . self::LOGINS_TABLE . ' SET auth_login_factors_done = auth_login_factors_done + 1 WHERE auth_login_id = ?'));
$stmt->addParameter(1, $login->getId(), DbType::STRING);
$stmt->execute();
}
}

View file

@ -0,0 +1,77 @@
<?php
namespace Hanyuu\Auth\Db;
use Index\DateTime;
use Index\Data\IDbResult;
use Index\Net\IPAddress;
use Hanyuu\Auth\IAuthLogin;
class DbAuthLogin implements IAuthLogin {
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;
}
}

19
src/Auth/IAuthLogin.php Normal file
View file

@ -0,0 +1,19 @@
<?php
namespace Hanyuu\Auth;
use Index\DateTime;
use Index\Net\IPAddress;
interface IAuthLogin {
public function getId(): string;
public function getUserId(): string;
public function getRemoteAddress(): IPAddress;
public function getCountryCode(): string;
public function getFactorsRequired(): int;
public function getFactorsDone(): int;
public function getStartedTime(): DateTime;
public function isValid(): bool;
public function getValidTime(): DateTime;
public function hasCompleted(): bool;
public function getCompletedTime(): DateTime;
}

View file

@ -8,6 +8,9 @@ use Index\Data\Migration\FsDbMigrationRepo;
use Index\Http\HttpFx;
use Index\Http\HttpRequest;
use Index\Routing\IRouter;
use Hanyuu\Auth\Auth;
use Hanyuu\Auth\AuthRoutes;
use Hanyuu\Auth\Db\DbAuth;
use Hanyuu\Config\IConfig;
use Hanyuu\Templating\TemplateContext;
use Hanyuu\Users\IUsers;
@ -17,6 +20,7 @@ class HanyuuContext {
private IConfig $config;
private IDbConnection $dbConn;
private IUsers $users;
private Auth $auth;
private ?TemplateContext $tpl = null;
public function __construct(IConfig $config, IDbConnection $dbConn) {
@ -63,6 +67,14 @@ class HanyuuContext {
return $this->getTemplating()->render(...$args);
}
public function setUpAuth(): void {
$this->auth = new DbAuth($this->dbConn);
}
public function getAuth(): Auth {
return $this->auth;
}
public function getRouter(): IRouter {
return $this->router->getRouter();
}
@ -107,25 +119,8 @@ class HanyuuContext {
return 503;
});
if(!HAU_DEBUG)
return;
$this->router->get('/coffee', function() { return 418; });
$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);
});
new AuthRoutes($this->router, $this, $this->config->scopeTo('auth'));
}
}

27
src/StatementCache.php Normal file
View file

@ -0,0 +1,27 @@
<?php
namespace Hanyuu;
use Index\Data\IDbConnection;
use Index\Data\IDbStatement;
class StatementCache {
private array $statements = [];
public function __construct(
private IDbConnection $conn
) {}
public function getStatement(string $name, callable $query): IDbStatement {
if(array_key_exists($name, $this->statements)) {
$stmt = $this->statements[$name];
$stmt->reset();
} else
$this->statements[$name] = $stmt = $this->conn->prepare($query());
return $stmt;
}
public function clearStatement(string $name): void {
unset($this->statements[$name]);
}
}

View file

@ -32,4 +32,14 @@ class DbUserTOTPInfo implements IUserTOTPInfo {
public function createGenerator(): TOTPGenerator {
return new TOTPGenerator($this->secretKey);
}
public function generateValidCodes(int $offset = 0, int $timeStamp = -1): array {
$generator = $this->createGenerator();
return [
$generator->generate(-1 + $offset, $timeStamp),
$generator->generate($offset, $timeStamp),
$generator->generate(1 + $offset, $timeStamp),
];
}
}

View file

@ -6,14 +6,18 @@ use Index\Data\IDbConnection;
use Index\Data\IDbStatement;
use Index\Data\IDbResult;
use Index\Data\DbType;
use Hanyuu\StatementCache;
use Hanyuu\Auth\IAuthMethod;
use Hanyuu\Auth\Auth;
use Hanyuu\Users\IUsers;
use Hanyuu\Users\IUserInfo;
use Hanyuu\Users\IUserPasswordInfo;
use Hanyuu\Users\IUserTOTPInfo;
use Hanyuu\Users\UserNotFoundException;
class DbUsers implements IUsers {
private const USERS_TABLE = 'hau_users';
private const AUTH_TABLE = 'hau_users_auth';
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';
@ -23,8 +27,8 @@ class DbUsers implements IUsers {
'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 AUTH_FIELDS = [
'user_id', 'user_auth_type', 'UNIX_TIMESTAMP(user_auth_enabled)',
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)',
@ -39,32 +43,23 @@ class DbUsers implements IUsers {
'user_id', 'user_backup_code', 'UNIX_TIMESTAMP(user_backup_created)', 'UNIX_TIMESTAMP(user_backup_used)',
];
private const FIRST_FACTOR_AUTH = ['pwd'];
private const SECOND_FACTOR_AUTH = ['totp'];
private StatementCache $stmts;
public function __construct(
private IDbConnection $conn
) {}
private array $statements = [];
private function getStatement(string $name, callable $query): IDbStatement {
if(array_key_exists($name, $this->statements))
return $this->statements[$name];
return $this->statements[$name] = $this->conn->prepare($query());
) {
$this->stmts = new StatementCache($conn);
}
private function fetchUserInfoSingle(IDbResult $result, string $exceptionText): IUserInfo {
if(!$result->next())
throw new RuntimeException($exceptionText);
throw new UserNotFoundException($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 = $this->stmts->getStatement('get info by id', fn() => ('SELECT ' . implode(',', self::USERS_FIELDS) . ' FROM ' . self::USERS_TABLE . ' WHERE user_id = ?'));
$stmt->addParameter(1, $userId, DbType::STRING);
$stmt->execute();
@ -72,8 +67,7 @@ class DbUsers implements IUsers {
}
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 = $this->stmts->getStatement('get info by name', fn() => ('SELECT ' . implode(',', self::USERS_FIELDS) . ' FROM ' . self::USERS_TABLE . ' WHERE user_name = ?'));
$stmt->addParameter(1, $userName, DbType::STRING);
$stmt->execute();
@ -81,7 +75,21 @@ class DbUsers implements IUsers {
}
private function fetchAuthInfoMultiple(IDbResult $result): array {
public function countUserTFAMethods(IUserInfo $userInfo): int {
$stmt = $this->stmts->getStatement('count user tfa methods', fn() => ('SELECT COUNT(*) FROM ' . self::TFA_TABLE . ' WHERE user_id = ?'));
$stmt->addParameter(1, $userInfo->getId(), DbType::STRING);
$stmt->execute();
$result = $stmt->getResult();
return $result->next() ? $result->getInteger(0) : 0;
}
public function getUserTFAMethods(IUserInfo $userInfo): array {
$stmt = $this->stmts->getStatement('get user tfa methods', fn() => ('SELECT ' . implode(',', self::TFA_FIELDS) . ' FROM ' . self::TFA_TABLE . ' WHERE user_id = ?'));
$stmt->addParameter(1, $userInfo->getId(), DbType::STRING);
$stmt->execute();
$result = $stmt->getResult();
$array = [];
while($result->next())
@ -90,28 +98,19 @@ class DbUsers implements IUsers {
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();
public function checkUserTFAMethod(IUserInfo $userInfo, IAuthMethod $method): bool {
$stmt = $this->stmts->getStatement('check user auth method', fn() => ('SELECT COUNT(*) FROM ' . self::TFA_TABLE . ' WHERE user_id = ? AND user_tfa_type = ?'));
$stmt->addParameter(1, $userInfo->getId(), DbType::STRING);
$stmt->addParameter(2, $method->getName(), DbType::STRING);
$stmt->execute();
$result = $stmt->getResult();
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());
return $result->next() ? $result->getInteger(0) !== 0 : false;
}
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 = $this->stmts->getStatement('get pwd', fn() => ('SELECT ' . implode(',', self::PASSWORDS_FIELDS) . ' FROM ' . self::PASSWORDS_TABLE . ' WHERE user_id = ?'));
$stmt->addParameter(1, $userInfo->getId(), DbType::STRING);
$stmt->execute();
$result = $stmt->getResult();
@ -124,8 +123,7 @@ class DbUsers implements IUsers {
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 = $this->stmts->getStatement('get totp', fn() => ('SELECT ' . implode(',', self::TOTP_FIELDS) . ' FROM ' . self::TOTP_TABLE . ' WHERE user_id = ?'));
$stmt->addParameter(1, $userInfo->getId(), DbType::STRING);
$stmt->execute();
$result = $stmt->getResult();
@ -138,8 +136,7 @@ class DbUsers implements IUsers {
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 = $this->stmts->getStatement('get emails', fn() => ('SELECT ' . implode(',', self::EMAILS_FIELDS) . ' FROM ' . self::EMAILS_TABLE . ' WHERE user_id = ?'));
$stmt->addParameter(1, $userInfo->getId(), DbType::STRING);
$stmt->execute();
$result = $stmt->getResult();
@ -154,8 +151,7 @@ class DbUsers implements IUsers {
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 = $this->stmts->getStatement('get backups', fn() => ('SELECT ' . implode(',', self::BACKUP_FIELDS) . ' FROM ' . self::BACKUP_TABLE . ' WHERE user_id = ?'));
$stmt->addParameter(1, $userInfo->getId(), DbType::STRING);
$stmt->execute();
$result = $stmt->getResult();

View file

@ -9,4 +9,5 @@ interface IUserTOTPInfo {
public function getSecretKey(): string;
public function getChangedTime(): DateTime;
public function createGenerator(): TOTPGenerator;
public function generateValidCodes(int $offset = 0, int $timeStamp = -1): array;
}

View file

@ -0,0 +1,6 @@
<?php
namespace Hanyuu\Users;
use RuntimeException;
class UserNotFoundException extends RuntimeException {}

View file

@ -0,0 +1,38 @@
<?php
$self->extends('auth/master');
$self->block('content', function() use ($self) {
?>
<form class="auth-login" method="post" action="/login/tfa">
<div class="auth-avatar">
<div class="auth-avatar-image">
<img src="//flashii.net/assets/avatar/<?=$self->userInfo->getId();?>?res=200" alt="">
</div>
</div>
<div class="auth-fields">
<label class="auth-input">
<div class="auth-input-title">
Username
</div>
<div class="auth-input-value">
<input type="text" value="<?=$self->userInfo->getName();?>" readonly>
</div>
</label>
</div>
<div class="auth-buttons">
<?php if(in_array('totp', $self->authMethodNames)): ?>
<a href="/login/tfa/totp" class="auth-button auth-button-primary">Authenticator app (TOTP)</a>
<?php endif; ?>
<?php if(in_array('u2f', $self->authMethodNames)): ?>
<a href="/login/tfa/u2f" class="auth-button auth-button-primary">Security key (U2F)</a>
<?php endif; ?>
<?php if(in_array('backup', $self->authMethodNames)): ?>
<a href="/login/tfa/backup" class="auth-button auth-button-primary">Backup code</a>
<?php endif; ?>
<a href="/login" class="auth-button">Cancel</a>
</div>
</form>
<?php
});

View file

@ -0,0 +1,39 @@
<?php
$self->extends('auth/master');
$self->block('content', function() use ($self) {
?>
<form class="auth-login" method="post" action="/login/tfa/totp">
<div class="auth-avatar">
<div class="auth-avatar-image">
<img src="//flashii.net/assets/avatar/<?=$self->userInfo->getId();?>?res=200" alt="">
</div>
</div>
<div class="auth-fields">
<label class="auth-input">
<div class="auth-input-title">
Username
</div>
<div class="auth-input-value">
<input type="text" value="<?=$self->userInfo->getName();?>" readonly>
</div>
</label>
<label class="auth-input">
<div class="auth-input-title">
Authenticator code
</div>
<div class="auth-input-value">
<input type="text" name="code" value="" maxlength="6" inputmode="numeric" placeholder="------">
</div>
</label>
</div>
<div class="auth-buttons">
<input type="submit" value="Next" class="auth-button auth-button-primary">
<a href="/login/tfa" class="auth-button">Back</a>
</div>
</form>
<?php
});

52
templates/auth/login.php Normal file
View file

@ -0,0 +1,52 @@
<?php
$self->extends('auth/master');
$self->registerSuffix = '';
$self->forgotSuffix = '';
if(!empty($self->userName)) {
$suffix = '?username=' . rawurlencode($self->userName);
if($self->errorId === 'user_not_found')
$self->registerSuffix = $suffix;
else
$self->forgotSuffix = $suffix;
}
$self->block('content', function() use ($self) {
?>
<form class="auth-login" method="post" action="/login">
<div class="auth-avatar">
<div class="auth-avatar-image">
<img src="//flashii.net/assets/avatar/0?res=200" alt="">
</div>
</div>
<div class="auth-fields">
<label class="auth-input">
<div class="auth-input-title">
Username
</div>
<div class="auth-input-value">
<input type="text" name="username" value="<?=$self->x($self->userName);?>" placeholder="example123">
</div>
</label>
<label class="auth-input">
<div class="auth-input-title">
Password
</div>
<div class="auth-input-value">
<input type="password" name="password" value="">
</div>
</label>
</div>
<div class="auth-buttons">
<input type="submit" value="Next" class="auth-button auth-button-primary">
<a href="/forgot-username<?=$self->forgotSuffix;?>" class="auth-button">Forgot username?</a>
<a href="/forgot-password<?=$self->forgotSuffix;?>" class="auth-button">Forgot password?</a>
<a href="/register<?=$self->registerSuffix;?>" class="auth-button">Create account</a>
</div>
</form>
<?php
});

13
templates/auth/master.php Normal file
View file

@ -0,0 +1,13 @@
<?php
$self->extends('master');
$self->block('body', function() use ($self) {
?>
<div class="auth">
<div class="auth-header">
<?=$self->hau->getSiteName();?>
</div>
<?=$self->getBlock('content');?>
</div>
<?php
});

View file

@ -3,7 +3,9 @@ $self->extends('master');
$self->block('body', function($self) {
?>
<h1><?=($self->http_error_title ?? 'Unknown Error');?></h1>
<p><?=($self->http_error_desc ?? 'No additional information is available.');?></p>
<div class="http-err">
<h1 class="http-err-title"><?=($self->http_error_title ?? 'Unknown Error');?></h1>
<p class="http-err-paragraph"><?=($self->http_error_desc ?? 'No additional information is available.');?></p>
</div>
<?php
});

View file

@ -4,8 +4,10 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title><?=$self->hau->getSiteName();?></title>
<link href="/assets/hanyuu.css" type="text/css" rel="stylesheet">
</head>
<body>
<?=$self->getBlock('body');?>
<script src="/assets/hanyuu.js" type="text/javascript"></script>
</body>
</html>

0
tools/migrate Executable file → Normal file
View file

0
tools/new-migration Executable file → Normal file
View file