UserToken / UserAPI

This commit is contained in:
Roman 2022-11-19 01:15:34 +01:00
parent f6bae08c05
commit b5b8f9b856
21 changed files with 496 additions and 613 deletions

@ -32,6 +32,7 @@ namespace Core\API {
$connectionData->setProperty("from", $settings["mail_from"] ?? ""); $connectionData->setProperty("from", $settings["mail_from"] ?? "");
$connectionData->setProperty("last_sync", $settings["mail_last_sync"] ?? ""); $connectionData->setProperty("last_sync", $settings["mail_last_sync"] ?? "");
$connectionData->setProperty("mail_footer", $settings["mail_footer"] ?? ""); $connectionData->setProperty("mail_footer", $settings["mail_footer"] ?? "");
$connectionData->setProperty("mail_async", $settings["mail_async"] ?? false);
return $connectionData; return $connectionData;
} }
@ -89,7 +90,7 @@ namespace Core\API\Mail {
'replyTo' => new Parameter('replyTo', Parameter::TYPE_EMAIL, true, null), 'replyTo' => new Parameter('replyTo', Parameter::TYPE_EMAIL, true, null),
'replyName' => new StringType('replyName', 32, true, ""), 'replyName' => new StringType('replyName', 32, true, ""),
'gpgFingerprint' => new StringType("gpgFingerprint", 64, true, null), 'gpgFingerprint' => new StringType("gpgFingerprint", 64, true, null),
'async' => new Parameter("async", Parameter::TYPE_BOOLEAN, true, true) 'async' => new Parameter("async", Parameter::TYPE_BOOLEAN, true, null)
)); ));
$this->isPublic = false; $this->isPublic = false;
} }
@ -110,7 +111,13 @@ namespace Core\API\Mail {
$body = $this->getParam('body'); $body = $this->getParam('body');
$gpgFingerprint = $this->getParam("gpgFingerprint"); $gpgFingerprint = $this->getParam("gpgFingerprint");
if ($this->getParam("async")) { $mailAsync = $this->getParam("async");
if ($mailAsync === null) {
// not set? grab from settings
$mailAsync = $mailConfig->getProperty("mail_async", false);
}
if ($mailAsync) {
$sql = $this->context->getSQL(); $sql = $this->context->getSQL();
$this->success = $sql->insert("MailQueue", ["from", "to", "subject", "body", $this->success = $sql->insert("MailQueue", ["from", "to", "subject", "body",
"replyTo", "replyName", "gpgFingerprint"]) "replyTo", "replyName", "gpgFingerprint"])

@ -223,7 +223,7 @@ abstract class Request {
} }
// Check for permission // Check for permission
if (!($this instanceof \API\Permission\Save)) { if (!($this instanceof \Core\API\Permission\Save)) {
$req = new \Core\API\Permission\Check($this->context); $req = new \Core\API\Permission\Check($this->context);
$this->success = $req->execute(array("method" => $this->getMethod())); $this->success = $req->execute(array("method" => $this->getMethod()));
$this->lastError = $req->getLastError(); $this->lastError = $req->getLastError();
@ -242,8 +242,8 @@ abstract class Request {
} }
$sql = $this->context->getSQL(); $sql = $this->context->getSQL();
if (!$sql->isConnected()) { if ($sql === null || !$sql->isConnected()) {
$this->lastError = $sql->getLastError(); $this->lastError = $sql ? $sql->getLastError() : "Database not connected yet.";
return false; return false;
} }
@ -265,8 +265,8 @@ abstract class Request {
return false; return false;
} }
protected function getParam($name, $obj = NULL) { protected function getParam($name, $obj = NULL): mixed {
// i don't know why phpstorm // I don't know why phpstorm
if ($obj === NULL) { if ($obj === NULL) {
$obj = $this->params; $obj = $this->params;
} }

@ -45,16 +45,23 @@ namespace Core\API\Template {
return $this->createError("Invalid template file extension. Allowed: " . implode(",", $allowedExtensions)); return $this->createError("Invalid template file extension. Allowed: " . implode(",", $allowedExtensions));
} }
$templateDir = WEBROOT . "/Core/Templates/";
$templateCache = WEBROOT . "/Core/Cache/Templates/"; $templateCache = WEBROOT . "/Core/Cache/Templates/";
$path = realpath($templateDir . $templateFile); $baseDirs = ["Site", "Core"];
if (!startsWith($path, realpath($templateDir))) { $valid = false;
return $this->createError("Template file not in template directory");
} else if (!is_file($path)) { foreach ($baseDirs as $baseDir) {
return $this->createError("Template file not found"); $path = realpath(implode("/", [WEBROOT, $baseDir, "Templates", $templateFile]));
if ($path && is_file($path)) {
$valid = true;
break;
}
} }
$twigLoader = new FilesystemLoader($templateDir); if (!$valid) {
return $this->createError("Template file not found or not inside template directory");
}
$twigLoader = new FilesystemLoader(dirname($path));
$twigEnvironment = new Environment($twigLoader, [ $twigEnvironment = new Environment($twigLoader, [
'cache' => $templateCache, 'cache' => $templateCache,
'auto_reload' => true 'auto_reload' => true

@ -123,10 +123,11 @@ namespace Core\API\TFA {
if ($this->success) { if ($this->success) {
$body = $req->getResult()["html"]; $body = $req->getResult()["html"];
$gpg = $currentUser->getGPG(); $gpg = $currentUser->getGPG();
$siteName = $settings->getSiteName();
$req = new \Core\API\Mail\Send($this->context); $req = new \Core\API\Mail\Send($this->context);
$this->success = $req->execute([ $this->success = $req->execute([
"to" => $currentUser->getEmail(), "to" => $currentUser->getEmail(),
"subject" => "[Security Lab] 2FA-Authentication removed", "subject" => "[$siteName] 2FA-Authentication removed",
"body" => $body, "body" => $body,
"gpgFingerprint" => $gpg?->getFingerprint() "gpgFingerprint" => $gpg?->getFingerprint()
]); ]);

File diff suppressed because it is too large Load Diff

@ -19,14 +19,6 @@ class CreateDatabase extends DatabaseScript {
->addRow("en_US", 'American English') ->addRow("en_US", 'American English')
->addRow("de_DE", 'Deutsch Standard'); ->addRow("de_DE", 'Deutsch Standard');
$queries[] = $sql->createTable("UserToken")
->addInt("user_id")
->addString("token", 36)
->addEnum("token_type", array("password_reset", "email_confirm", "invite", "gpg_confirm"))
->addDateTime("valid_until")
->addBool("used", false)
->foreignKey("user_id", "User", "id", new CascadeStrategy());
$queries[] = $sql->insert("Group", array("name", "color")) $queries[] = $sql->insert("Group", array("name", "color"))
->addRow(USER_GROUP_MODERATOR_NAME, "#007bff") ->addRow(USER_GROUP_MODERATOR_NAME, "#007bff")
->addRow(USER_GROUP_SUPPORT_NAME, "#28a745") ->addRow(USER_GROUP_SUPPORT_NAME, "#28a745")

@ -15,20 +15,28 @@ class Settings {
// //
private bool $installationComplete; private bool $installationComplete;
// settings // general settings
private string $siteName; private string $siteName;
private string $baseUrl; private string $baseUrl;
private bool $registrationAllowed;
private array $allowedExtensions;
private string $timeZone;
// jwt
private ?string $jwtPublicKey; private ?string $jwtPublicKey;
private ?string $jwtSecretKey; private ?string $jwtSecretKey;
private string $jwtAlgorithm; private string $jwtAlgorithm;
private bool $registrationAllowed;
// recaptcha
private bool $recaptchaEnabled; private bool $recaptchaEnabled;
private bool $mailEnabled;
private string $recaptchaPublicKey; private string $recaptchaPublicKey;
private string $recaptchaPrivateKey; private string $recaptchaPrivateKey;
// mail
private bool $mailEnabled;
private string $mailSender; private string $mailSender;
private string $mailFooter; private string $mailFooter;
private array $allowedExtensions; private bool $mailAsync;
// //
private Logger $logger; private Logger $logger;
@ -55,7 +63,11 @@ class Settings {
} }
public static function loadDefaults(): Settings { public static function loadDefaults(): Settings {
$hostname = $_SERVER["SERVER_NAME"] ?? "localhost"; $hostname = $_SERVER["SERVER_NAME"];
if (empty($hostname)) {
$hostname = "localhost";
}
$protocol = getProtocol(); $protocol = getProtocol();
$settings = new Settings(); $settings = new Settings();
@ -65,6 +77,7 @@ class Settings {
$settings->allowedExtensions = ['png', 'jpg', 'jpeg', 'gif', 'htm', 'html']; $settings->allowedExtensions = ['png', 'jpg', 'jpeg', 'gif', 'htm', 'html'];
$settings->installationComplete = false; $settings->installationComplete = false;
$settings->registrationAllowed = false; $settings->registrationAllowed = false;
$settings->timeZone = date_default_timezone_get();
// JWT // JWT
$settings->jwtSecretKey = null; $settings->jwtSecretKey = null;
@ -80,7 +93,7 @@ class Settings {
$settings->mailEnabled = false; $settings->mailEnabled = false;
$settings->mailSender = "webmaster@localhost"; $settings->mailSender = "webmaster@localhost";
$settings->mailFooter = ""; $settings->mailFooter = "";
$settings->mailAsync = false;
return $settings; return $settings;
} }
@ -118,7 +131,7 @@ class Settings {
return in_array(strtoupper($algorithm), ["HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "EDDSA"]); return in_array(strtoupper($algorithm), ["HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "EDDSA"]);
} }
public function saveJwtKey(Context $context) { public function saveJwtKey(Context $context): \Core\API\Settings\Set {
$req = new \Core\API\Settings\Set($context); $req = new \Core\API\Settings\Set($context);
$req->execute(array("settings" => array( $req->execute(array("settings" => array(
"jwt_secret_key" => $this->jwtSecretKey, "jwt_secret_key" => $this->jwtSecretKey,
@ -140,6 +153,7 @@ class Settings {
$this->baseUrl = $result["base_url"] ?? $this->baseUrl; $this->baseUrl = $result["base_url"] ?? $this->baseUrl;
$this->registrationAllowed = $result["user_registration_enabled"] ?? $this->registrationAllowed; $this->registrationAllowed = $result["user_registration_enabled"] ?? $this->registrationAllowed;
$this->installationComplete = $result["installation_completed"] ?? $this->installationComplete; $this->installationComplete = $result["installation_completed"] ?? $this->installationComplete;
$this->timeZone = $result["time_zone"] ?? $this->timeZone;
$this->jwtSecretKey = $result["jwt_secret_key"] ?? $this->jwtSecretKey; $this->jwtSecretKey = $result["jwt_secret_key"] ?? $this->jwtSecretKey;
$this->jwtPublicKey = $result["jwt_public_key"] ?? $this->jwtPublicKey; $this->jwtPublicKey = $result["jwt_public_key"] ?? $this->jwtPublicKey;
$this->jwtAlgorithm = $result["jwt_algorithm"] ?? $this->jwtAlgorithm; $this->jwtAlgorithm = $result["jwt_algorithm"] ?? $this->jwtAlgorithm;
@ -149,6 +163,7 @@ class Settings {
$this->mailEnabled = $result["mail_enabled"] ?? $this->mailEnabled; $this->mailEnabled = $result["mail_enabled"] ?? $this->mailEnabled;
$this->mailSender = $result["mail_from"] ?? $this->mailSender; $this->mailSender = $result["mail_from"] ?? $this->mailSender;
$this->mailFooter = $result["mail_footer"] ?? $this->mailFooter; $this->mailFooter = $result["mail_footer"] ?? $this->mailFooter;
$this->mailAsync = $result["mail_async"] ?? $this->mailAsync;
$this->allowedExtensions = explode(",", $result["allowed_extensions"] ?? strtolower(implode(",", $this->allowedExtensions))); $this->allowedExtensions = explode(",", $result["allowed_extensions"] ?? strtolower(implode(",", $this->allowedExtensions)));
if (!isset($result["jwt_secret_key"])) { if (!isset($result["jwt_secret_key"])) {
@ -156,16 +171,19 @@ class Settings {
$this->saveJwtKey($context); $this->saveJwtKey($context);
} }
} }
date_default_timezone_set($this->timeZone);
} }
return false; return false;
} }
public function addRows(Insert $query) { public function addRows(Insert $query): void {
$query->addRow("site_name", $this->siteName, false, false) $query->addRow("site_name", $this->siteName, false, false)
->addRow("base_url", $this->baseUrl, false, false) ->addRow("base_url", $this->baseUrl, false, false)
->addRow("user_registration_enabled", $this->registrationAllowed ? "1" : "0", false, false) ->addRow("user_registration_enabled", $this->registrationAllowed ? "1" : "0", false, false)
->addRow("installation_completed", $this->installationComplete ? "1" : "0", true, true) ->addRow("installation_completed", $this->installationComplete ? "1" : "0", true, true)
->addRow("time_zone", $this->timeZone, false, false)
->addRow("jwt_secret_key", $this->jwtSecretKey, true, false) ->addRow("jwt_secret_key", $this->jwtSecretKey, true, false)
->addRow("jwt_public_key", $this->jwtPublicKey, false, false) ->addRow("jwt_public_key", $this->jwtPublicKey, false, false)
->addRow("jwt_algorithm", $this->jwtAlgorithm, false, false) ->addRow("jwt_algorithm", $this->jwtAlgorithm, false, false)
@ -179,6 +197,14 @@ class Settings {
return $this->siteName; return $this->siteName;
} }
public function getTimeZone(): string {
return $this->timeZone;
}
public function setTimeZone(string $tz) {
$this->timeZone = $tz;
}
public function getBaseUrl(): string { public function getBaseUrl(): string {
return $this->baseUrl; return $this->baseUrl;
} }
@ -203,6 +229,10 @@ class Settings {
return $this->mailEnabled; return $this->mailEnabled;
} }
public function isMailAsync(): bool {
return $this->mailAsync;
}
public function getMailSender(): string { public function getMailSender(): string {
return $this->mailSender; return $this->mailSender;
} }

@ -235,7 +235,7 @@ namespace Documents\Install {
$username = posix_getpwuid($userId)['name']; $username = posix_getpwuid($userId)['name'];
$failedRequirements[] = sprintf("<b>%s</b> is not owned by current user: $username ($userId). " . $failedRequirements[] = sprintf("<b>%s</b> is not owned by current user: $username ($userId). " .
"Try running <b>chown -R $userId %s</b> or give the required directories write permissions: " . "Try running <b>chown -R $userId %s</b> or give the required directories write permissions: " .
"<b>core/Configuration</b>, <b>core/Cache</b>, <b>core/External</b>", "<b>Site/Configuration</b>, <b>Core/Cache</b>, <b>Core/External</b>",
WEBROOT, WEBROOT); WEBROOT, WEBROOT);
$success = false; $success = false;
} }

@ -72,7 +72,7 @@ class Logger {
$module = preg_replace("/[^a-zA-Z0-9-]/", "-", $this->module); $module = preg_replace("/[^a-zA-Z0-9-]/", "-", $this->module);
$date = (\DateTime::createFromFormat('U.u', microtime(true)))->format(self::LOG_FILE_DATE_FORMAT); $date = (\DateTime::createFromFormat('U.u', microtime(true)))->format(self::LOG_FILE_DATE_FORMAT);
$logFile = implode("_", [$module, $severity, $date]) . ".log"; $logFile = implode("_", [$module, $severity, $date]) . ".log";
$logPath = implode(DIRECTORY_SEPARATOR, [WEBROOT, "core", "Logs", $logFile]); $logPath = implode(DIRECTORY_SEPARATOR, [WEBROOT, "Core", "Logs", $logFile]);
@file_put_contents($logPath, $message); @file_put_contents($logPath, $message);
} }

@ -31,7 +31,7 @@ class TemplateDocument extends Document {
$this->parameters = $params; $this->parameters = $params;
$this->twigLoader = new FilesystemLoader(self::TEMPLATE_PATH); $this->twigLoader = new FilesystemLoader(self::TEMPLATE_PATH);
$this->twigEnvironment = new Environment($this->twigLoader, [ $this->twigEnvironment = new Environment($this->twigLoader, [
'cache' => WEBROOT . '/core/Cache/Templates/', 'cache' => WEBROOT . '/Core/Cache/Templates/',
'auto_reload' => true 'auto_reload' => true
]); ]);
$this->twigEnvironment->addExtension(new CustomTwigFunctions()); $this->twigEnvironment->addExtension(new CustomTwigFunctions());

@ -7,6 +7,7 @@ use Core\Configuration\Settings;
use Core\Driver\SQL\Condition\Compare; use Core\Driver\SQL\Condition\Compare;
use Core\Driver\SQL\Condition\CondLike; use Core\Driver\SQL\Condition\CondLike;
use Core\Driver\SQL\Condition\CondOr; use Core\Driver\SQL\Condition\CondOr;
use Core\Driver\SQL\Join;
use Core\Driver\SQL\SQL; use Core\Driver\SQL\SQL;
use Firebase\JWT\JWT; use Firebase\JWT\JWT;
use Core\Objects\DatabaseEntity\Language; use Core\Objects\DatabaseEntity\Language;
@ -92,6 +93,9 @@ class Context {
private function loadSession(int $userId, int $sessionId) { private function loadSession(int $userId, int $sessionId) {
$this->session = Session::init($this, $userId, $sessionId); $this->session = Session::init($this, $userId, $sessionId);
$this->user = $this->session?->getUser(); $this->user = $this->session?->getUser();
if ($this->user) {
$this->user->session = $this->session;
}
} }
public function parseCookies() { public function parseCookies() {
@ -173,7 +177,7 @@ class Context {
public function loadApiKey(string $apiKey): bool { public function loadApiKey(string $apiKey): bool {
$this->user = User::findBuilder($this->sql) $this->user = User::findBuilder($this->sql)
->addJoin(new \Driver\SQL\Join("INNER","ApiKey", "ApiKey.user_id", "User.id")) ->addJoin(new Join("INNER","ApiKey", "ApiKey.user_id", "User.id"))
->where(new Compare("ApiKey.api_key", $apiKey)) ->where(new Compare("ApiKey.api_key", $apiKey))
->where(new Compare("valid_until", $this->sql->currentTimestamp(), ">")) ->where(new Compare("valid_until", $this->sql->currentTimestamp(), ">"))
->where(new Compare("ApiKey.active", true)) ->where(new Compare("ApiKey.active", true))
@ -184,20 +188,19 @@ class Context {
return $this->user !== null; return $this->user !== null;
} }
public function createSession(int $userId, bool $stayLoggedIn): ?Session { public function createSession(User $user, bool $stayLoggedIn): ?Session {
$this->user = User::find($this->sql, $userId); $this->user = $user;
if ($this->user) {
$this->session = new Session($this, $this->user); $this->session = new Session($this, $this->user);
$this->session->stayLoggedIn = $stayLoggedIn; $this->session->stayLoggedIn = $stayLoggedIn;
if ($this->session->update()) { if ($this->session->update()) {
$user->session = $this->session;
return $this->session; return $this->session;
} } else {
}
$this->user = null; $this->user = null;
$this->session = null; $this->session = null;
return null; return null;
} }
}
public function getLanguage(): Language { public function getLanguage(): Language {
return $this->language; return $this->language;

@ -0,0 +1,12 @@
<?php
namespace Core\Objects\DatabaseEntity\Attribute;
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class EnumArr extends Enum {
public function __construct(array $values) {
parent::__construct(...$values);
}
}

@ -322,7 +322,7 @@ class DatabaseEntityHandler {
if ($property->isInitialized($entity)) { if ($property->isInitialized($entity)) {
$value = $property->getValue($entity); $value = $property->getValue($entity);
if (isset($this->relations[$propertyName])) { if (isset($this->relations[$propertyName])) {
$value = $value->getId(); $value = $value?->getId();
} }
} else if (!$this->columns[$propertyName]->notNull()) { } else if (!$this->columns[$propertyName]->notNull()) {
$value = null; $value = null;
@ -411,4 +411,8 @@ class DatabaseEntityHandler {
$this->logger->error($message); $this->logger->error($message);
throw new Exception($message); throw new Exception($message);
} }
public function getSQL(): SQL {
return $this->sql;
}
} }

@ -2,6 +2,7 @@
namespace Core\Objects\DatabaseEntity; namespace Core\Objects\DatabaseEntity;
use Core\Driver\Logger\Logger;
use Core\Driver\SQL\Condition\Condition; use Core\Driver\SQL\Condition\Condition;
use Core\Driver\SQL\Join; use Core\Driver\SQL\Join;
use Core\Driver\SQL\Query\Select; use Core\Driver\SQL\Query\Select;
@ -13,20 +14,29 @@ use Core\Driver\SQL\SQL;
*/ */
class DatabaseEntityQuery { class DatabaseEntityQuery {
private Logger $logger;
private DatabaseEntityHandler $handler; private DatabaseEntityHandler $handler;
private Select $selectQuery; private Select $selectQuery;
private int $resultType; private int $resultType;
private bool $logVerbose;
private function __construct(DatabaseEntityHandler $handler, int $resultType) { private function __construct(DatabaseEntityHandler $handler, int $resultType) {
$this->handler = $handler; $this->handler = $handler;
$this->selectQuery = $handler->getSelectQuery(); $this->selectQuery = $handler->getSelectQuery();
$this->logger = new Logger("DB-EntityQuery", $handler->getSQL());
$this->resultType = $resultType; $this->resultType = $resultType;
$this->logVerbose = false;
if ($this->resultType === SQL::FETCH_ONE) { if ($this->resultType === SQL::FETCH_ONE) {
$this->selectQuery->first(); $this->selectQuery->first();
} }
} }
public function debug(): DatabaseEntityQuery {
$this->logVerbose = true;
return $this;
}
public static function fetchAll(DatabaseEntityHandler $handler): DatabaseEntityQuery { public static function fetchAll(DatabaseEntityHandler $handler): DatabaseEntityQuery {
return new DatabaseEntityQuery($handler, SQL::FETCH_ALL); return new DatabaseEntityQuery($handler, SQL::FETCH_ALL);
} }
@ -106,6 +116,13 @@ class DatabaseEntityQuery {
} }
public function execute(): DatabaseEntity|array|null { public function execute(): DatabaseEntity|array|null {
if ($this->logVerbose) {
$params = [];
$query = $this->selectQuery->build($params);
$this->logger->debug("QUERY: $query\nARGS: " . print_r($params, true));
}
$res = $this->selectQuery->execute(); $res = $this->selectQuery->execute();
if ($res === null || $res === false) { if ($res === null || $res === false) {
return null; return null;

@ -3,6 +3,7 @@
namespace Core\Objects\DatabaseEntity; namespace Core\Objects\DatabaseEntity;
use Core\Driver\SQL\Expression\CurrentTimeStamp; use Core\Driver\SQL\Expression\CurrentTimeStamp;
use Core\Driver\SQL\SQL;
use Core\Objects\DatabaseEntity\Attribute\MaxLength; use Core\Objects\DatabaseEntity\Attribute\MaxLength;
use Core\Objects\DatabaseEntity\Attribute\DefaultValue; use Core\Objects\DatabaseEntity\Attribute\DefaultValue;
@ -16,12 +17,13 @@ class GpgKey extends DatabaseEntity {
private \DateTime $expires; private \DateTime $expires;
#[DefaultValue(CurrentTimeStamp::class)] private \DateTime $added; #[DefaultValue(CurrentTimeStamp::class)] private \DateTime $added;
public function __construct(int $id, bool $confirmed, string $fingerprint, string $algorithm, string $expires) { public function __construct(string $fingerprint, string $algorithm, \DateTime $expires) {
parent::__construct($id); parent::__construct();
$this->confirmed = $confirmed; $this->confirmed = false;
$this->fingerprint = $fingerprint; $this->fingerprint = $fingerprint;
$this->algorithm = $algorithm; $this->algorithm = $algorithm;
$this->expires = new \DateTime($expires); $this->expires = $expires;
$this->added = new \DateTime();
} }
public static function encrypt(string $body, string $gpgFingerprint): array { public static function encrypt(string $body, string $gpgFingerprint): array {
@ -130,4 +132,9 @@ class GpgKey extends DatabaseEntity {
"confirmed" => $this->confirmed "confirmed" => $this->confirmed
]; ];
} }
public function confirm(SQL $sql): bool {
$this->confirmed = true;
return $this->save($sql);
}
} }

@ -96,7 +96,7 @@ class Session extends DatabaseEntity {
return array( return array(
'id' => $this->getId(), 'id' => $this->getId(),
'active' => $this->active, 'active' => $this->active,
'expires' => $this->expires, 'expires' => $this->expires->getTimestamp(),
'ipAddress' => $this->ipAddress, 'ipAddress' => $this->ipAddress,
'os' => $this->os, 'os' => $this->os,
'browser' => $this->browser, 'browser' => $this->browser,

@ -17,12 +17,12 @@ class User extends DatabaseEntity {
#[MaxLength(128)] public string $password; #[MaxLength(128)] public string $password;
#[MaxLength(64)] public string $fullName; #[MaxLength(64)] public string $fullName;
#[MaxLength(64)] #[Unique] public ?string $email; #[MaxLength(64)] #[Unique] public ?string $email;
#[MaxLength(64)] private ?string $profilePicture; #[MaxLength(64)] public ?string $profilePicture;
private ?\DateTime $lastOnline; private ?\DateTime $lastOnline;
#[DefaultValue(CurrentTimeStamp::class)] public \DateTime $registeredAt; #[DefaultValue(CurrentTimeStamp::class)] public \DateTime $registeredAt;
public bool $confirmed; public bool $confirmed;
#[DefaultValue(1)] public Language $language; #[DefaultValue(1)] public Language $language;
private ?GpgKey $gpgKey; public ?GpgKey $gpgKey;
private ?TwoFactorToken $twoFactorToken; private ?TwoFactorToken $twoFactorToken;
#[Transient] private array $groups; #[Transient] private array $groups;
@ -37,7 +37,6 @@ class User extends DatabaseEntity {
$this->groups = []; $this->groups = [];
$groups = Group::findAllBuilder($sql) $groups = Group::findAllBuilder($sql)
->fetchEntities()
->addJoin(new Join("INNER", "UserGroup", "UserGroup.group_id", "Group.id")) ->addJoin(new Join("INNER", "UserGroup", "UserGroup.group_id", "Group.id"))
->where(new Compare("UserGroup.user_id", $this->id)) ->where(new Compare("UserGroup.user_id", $this->id))
->execute(); ->execute();
@ -99,6 +98,9 @@ class User extends DatabaseEntity {
'session' => (isset($this->session) ? $this->session->jsonSerialize() : null), 'session' => (isset($this->session) ? $this->session->jsonSerialize() : null),
"gpg" => (isset($this->gpgKey) ? $this->gpgKey->jsonSerialize() : null), "gpg" => (isset($this->gpgKey) ? $this->gpgKey->jsonSerialize() : null),
"2fa" => (isset($this->twoFactorToken) ? $this->twoFactorToken->jsonSerialize() : null), "2fa" => (isset($this->twoFactorToken) ? $this->twoFactorToken->jsonSerialize() : null),
"reqisteredAt" => $this->registeredAt->getTimestamp(),
"lastOnline" => $this->lastOnline->getTimestamp(),
"confirmed" => $this->confirmed
]; ];
} }

@ -0,0 +1,72 @@
<?php
namespace Core\Objects\DatabaseEntity;
use Core\Driver\SQL\SQL;
use Core\Objects\DatabaseEntity\Attribute\DefaultValue;
use Core\Objects\DatabaseEntity\Attribute\EnumArr;
use Core\Objects\DatabaseEntity\Attribute\MaxLength;
class UserToken extends DatabaseEntity {
const TYPE_PASSWORD_RESET = "password_reset";
const TYPE_EMAIL_CONFIRM = "email_confirm";
const TYPE_INVITE = "invite";
const TYPE_GPG_CONFIRM = "gpg_confirm";
const TOKEN_TYPES = [
self::TYPE_PASSWORD_RESET, self::TYPE_EMAIL_CONFIRM,
self::TYPE_INVITE, self::TYPE_GPG_CONFIRM
];
#[MaxLength(36)]
private string $token;
#[EnumArr(self::TOKEN_TYPES)]
private string $tokenType;
private User $user;
private \DateTime $validUntil;
#[DefaultValue(false)]
private bool $used;
public function __construct(User $user, string $token, string $type, int $validHours) {
parent::__construct();
$this->user = $user;
$this->token = $token;
$this->tokenType = $type;
$this->validUntil = (new \DateTime())->modify("+$validHours HOUR");
$this->used = false;
}
public function jsonSerialize(): array {
return [
"id" => $this->getId(),
"token" => $this->token,
"tokenType" => $this->tokenType
];
}
public function getType(): string {
return $this->tokenType;
}
public function invalidate(SQL $sql): bool {
$this->used = true;
return $this->save($sql);
}
public function getUser(): User {
return $this->user;
}
public function updateDurability(SQL $sql, int $validHours): bool {
$this->validUntil = (new \DateTime())->modify("+$validHours HOURS");
return $this->save($sql);
}
public function getToken(): string {
return $this->token;
}
}

0
Site/Templates/.gitkeep Normal file

25
cli.php

@ -48,17 +48,6 @@ if ($database !== null && $database->getProperty("isDocker", false) && !is_file(
} }
} }
/*function getUser(): ?User {
global $config;
$user = new User($config);
if (!$user->getSQL() || !$user->getSQL()->isConnected()) {
printLine("Could not establish database connection");
return null;
}
return $user;
}*/
function connectSQL(): ?SQL { function connectSQL(): ?SQL {
global $context; global $context;
$sql = $context->initSQL(); $sql = $context->initSQL();
@ -76,7 +65,7 @@ function printHelp() {
function applyPatch(\Core\Driver\SQL\SQL $sql, string $patchName): bool { function applyPatch(\Core\Driver\SQL\SQL $sql, string $patchName): bool {
$class = str_replace('/', '\\', $patchName); $class = str_replace('/', '\\', $patchName);
$className = "\\Configuration\\$class"; $className = "\\Core\\Configuration\\$class";
$classPath = getClassPath($className); $classPath = getClassPath($className);
if (!file_exists($classPath) || !is_readable($classPath)) { if (!file_exists($classPath) || !is_readable($classPath)) {
printLine("Database script file does not exist or is not readable"); printLine("Database script file does not exist or is not readable");
@ -282,7 +271,7 @@ function onMaintenance(array $argv) {
_exit("Maintenance disabled"); _exit("Maintenance disabled");
} else if ($action === "update") { } else if ($action === "update") {
$oldPatchFiles = glob('core/Configuration/Patch/*.php'); $oldPatchFiles = glob('Core/Configuration/Patch/*.php');
printLine("$ git remote -v"); printLine("$ git remote -v");
exec("git remote -v", $gitRemote, $ret); exec("git remote -v", $gitRemote, $ret);
if ($ret !== 0) { if ($ret !== 0) {
@ -339,14 +328,15 @@ function onMaintenance(array $argv) {
die(); die();
} }
$newPatchFiles = glob('core/Configuration/Patch/*.php'); // TODO: also collect patches from Site/Configuration/Patch ... and what about database entities?
$newPatchFiles = glob('Core/Configuration/Patch/*.php');
$newPatchFiles = array_diff($newPatchFiles, $oldPatchFiles); $newPatchFiles = array_diff($newPatchFiles, $oldPatchFiles);
if (count($newPatchFiles) > 0) { if (count($newPatchFiles) > 0) {
printLine("Applying new database patches"); printLine("Applying new database patches");
$sql = connectSQL(); $sql = connectSQL();
if ($sql) { if ($sql) {
foreach ($newPatchFiles as $patchFile) { foreach ($newPatchFiles as $patchFile) {
if (preg_match("/core\/Configuration\/(Patch\/.*)\.class\.php/", $patchFile, $match)) { if (preg_match("/Core\/Configuration\/(Patch\/.*)\.class\.php/", $patchFile, $match)) {
$patchName = $match[1]; $patchName = $match[1];
applyPatch($sql, $patchName); applyPatch($sql, $patchName);
} }
@ -415,7 +405,7 @@ function printTable(array $head, array $body) {
function onSettings(array $argv) { function onSettings(array $argv) {
global $context; global $context;
$sql = connectSQL() or die(); connectSQL() or die();
$action = $argv[2] ?? "list"; $action = $argv[2] ?? "list";
if ($action === "list" || $action === "get") { if ($action === "list" || $action === "get") {
@ -461,7 +451,7 @@ function onSettings(array $argv) {
function onRoutes(array $argv) { function onRoutes(array $argv) {
global $context; global $context;
$sql = connectSQL() or die(); connectSQL() or die();
$action = $argv[2] ?? "list"; $action = $argv[2] ?? "list";
if ($action === "list") { if ($action === "list") {
@ -607,6 +597,7 @@ function onMail($argv) {
global $context; global $context;
$action = $argv[2] ?? null; $action = $argv[2] ?? null;
if ($action === "send_queue") { if ($action === "send_queue") {
connectSQL() or die();
$req = new \Core\API\Mail\SendQueue($context); $req = new \Core\API\Mail\SendQueue($context);
$debug = in_array("debug", $argv); $debug = in_array("debug", $argv);
if (!$req->execute(["debug" => $debug])) { if (!$req->execute(["debug" => $debug])) {

@ -1,11 +1,14 @@
FROM composer:latest AS composer FROM composer:latest AS composer
FROM php:8.0-fpm FROM php:8.0-fpm
WORKDIR "/application" WORKDIR "/application"
RUN mkdir -p /application/core/Configuration RUN mkdir -p /application/core/Configuration /var/www/.gnupg && \
RUN chown -R www-data:www-data /application chown -R www-data:www-data /application /var/www/ && \
chmod 700 /var/www/.gnupg
# YAML + dev dependencies # YAML + dev dependencies
RUN apt-get update -y && apt-get install libyaml-dev libzip-dev libgmp-dev -y && apt-get clean && \ RUN apt-get update -y && \
apt-get install -y libyaml-dev libzip-dev libgmp-dev gnupg2 && \
apt-get clean && \
pecl install yaml && docker-php-ext-enable yaml pecl install yaml && docker-php-ext-enable yaml
# Runkit (no stable release available) # Runkit (no stable release available)