Swagger update + moved API user/gpg -> gpgkey/
This commit is contained in:
parent
8036edec5a
commit
d6c6572989
295
Core/API/GpgKeyAPI.class.php
Normal file
295
Core/API/GpgKeyAPI.class.php
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Core\API {
|
||||||
|
|
||||||
|
use Core\Objects\Context;
|
||||||
|
|
||||||
|
abstract class GpgKeyAPI extends \Core\API\Request {
|
||||||
|
public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
|
||||||
|
parent::__construct($context, $externalCall, $params);
|
||||||
|
$this->loginRequired = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Core\API\GpgKey {
|
||||||
|
|
||||||
|
|
||||||
|
use Core\API\GpgKeyAPI;
|
||||||
|
use Core\API\Parameter\Parameter;
|
||||||
|
use Core\API\Parameter\StringType;
|
||||||
|
use Core\API\Template\Render;
|
||||||
|
use Core\Driver\SQL\Condition\Compare;
|
||||||
|
use Core\Driver\SQL\Query\Insert;
|
||||||
|
use Core\Objects\Context;
|
||||||
|
use Core\Objects\DatabaseEntity\GpgKey;
|
||||||
|
use Core\Objects\DatabaseEntity\User;
|
||||||
|
use Core\Objects\DatabaseEntity\UserToken;
|
||||||
|
|
||||||
|
class Import extends GpgKeyAPI {
|
||||||
|
|
||||||
|
public function __construct(Context $context, bool $externalCall = false) {
|
||||||
|
parent::__construct($context, $externalCall, [
|
||||||
|
"pubkey" => new StringType("pubkey")
|
||||||
|
]);
|
||||||
|
$this->loginRequired = true;
|
||||||
|
$this->forbidMethod("GET");
|
||||||
|
}
|
||||||
|
|
||||||
|
private function testKey(string $keyString) {
|
||||||
|
$res = GpgKey::getKeyInfo($keyString);
|
||||||
|
if (!$res["success"]) {
|
||||||
|
return $this->createError($res["error"] ?? $res["msg"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$keyData = $res["data"];
|
||||||
|
$keyType = $keyData["type"];
|
||||||
|
$expires = $keyData["expires"];
|
||||||
|
|
||||||
|
if ($keyType === "sec#") {
|
||||||
|
return self::createError("ATTENTION! It seems like you've imported a PGP PRIVATE KEY instead of a public key.
|
||||||
|
It is recommended to immediately revoke your private key and create a new key pair.");
|
||||||
|
} else if ($keyType !== "pub") {
|
||||||
|
return self::createError("Unknown key type: $keyType");
|
||||||
|
} else if (isInPast($expires)) {
|
||||||
|
return self::createError("It seems like the gpg key is already expired.");
|
||||||
|
} else {
|
||||||
|
return $keyData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function _execute(): bool {
|
||||||
|
|
||||||
|
$currentUser = $this->context->getUser();
|
||||||
|
$gpgKey = $currentUser->getGPG();
|
||||||
|
if ($gpgKey) {
|
||||||
|
return $this->createError("You already added a GPG key to your account.");
|
||||||
|
} else if (!$currentUser->getEmail()) {
|
||||||
|
return $this->createError("You do not have an e-mail address");
|
||||||
|
}
|
||||||
|
|
||||||
|
// fix key first, enforce a newline after
|
||||||
|
$keyString = $this->getParam("pubkey");
|
||||||
|
$keyString = preg_replace("/(-{2,})\n([^\n])/", "$1\n\n$2", $keyString);
|
||||||
|
$keyData = $this->testKey($keyString);
|
||||||
|
if ($keyData === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$res = GpgKey::importKey($keyString);
|
||||||
|
if (!$res["success"]) {
|
||||||
|
return $this->createError($res["error"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = $this->context->getSQL();
|
||||||
|
$gpgKey = new GpgKey($keyData["fingerprint"], $keyData["algorithm"], $keyData["expires"]);
|
||||||
|
if (!$gpgKey->save($sql)) {
|
||||||
|
return $this->createError("Error creating gpg key: " . $sql->getLastError());
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = generateRandomString(36);
|
||||||
|
$userToken = new UserToken($currentUser, $token, UserToken::TYPE_GPG_CONFIRM, 1);
|
||||||
|
if (!$userToken->save($sql)) {
|
||||||
|
return $this->createError("Error saving user token: " . $sql->getLastError());
|
||||||
|
}
|
||||||
|
|
||||||
|
$validHours = 1;
|
||||||
|
$settings = $this->context->getSettings();
|
||||||
|
$baseUrl = $settings->getBaseUrl();
|
||||||
|
$siteName = $settings->getSiteName();
|
||||||
|
$req = new Render($this->context);
|
||||||
|
$this->success = $req->execute([
|
||||||
|
"file" => "mail/gpg_import.twig",
|
||||||
|
"parameters" => [
|
||||||
|
"link" => "$baseUrl/resetPassword?token=$token",
|
||||||
|
"site_name" => $siteName,
|
||||||
|
"base_url" => $baseUrl,
|
||||||
|
"username" => $currentUser->getDisplayName(),
|
||||||
|
"valid_time" => $this->formatDuration($validHours, "hour")
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->lastError = $req->getLastError();
|
||||||
|
|
||||||
|
if ($this->success) {
|
||||||
|
$messageBody = $req->getResult()["html"];
|
||||||
|
$sendMail = new \Core\API\Mail\Send($this->context);
|
||||||
|
$this->success = $sendMail->execute(array(
|
||||||
|
"to" => $currentUser->getEmail(),
|
||||||
|
"subject" => "[$siteName] Confirm GPG-Key",
|
||||||
|
"body" => $messageBody,
|
||||||
|
"gpgFingerprint" => $gpgKey->getFingerprint()
|
||||||
|
));
|
||||||
|
|
||||||
|
$this->lastError = $sendMail->getLastError();
|
||||||
|
|
||||||
|
if ($this->success) {
|
||||||
|
$currentUser->gpgKey = $gpgKey;
|
||||||
|
if ($currentUser->save($sql, ["gpgKey"])) {
|
||||||
|
$this->result["gpgKey"] = $gpgKey->jsonSerialize();
|
||||||
|
} else {
|
||||||
|
return $this->createError("Error updating user details: " . $sql->getLastError());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getDefaultACL(Insert $insert): void {
|
||||||
|
$insert->addRow(self::getEndpoint(), [], "Allows users to import gpg keys for a secure e-mail communication", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Remove extends GpgKeyAPI {
|
||||||
|
public function __construct(Context $context, bool $externalCall = false) {
|
||||||
|
parent::__construct($context, $externalCall, array(
|
||||||
|
"password" => new StringType("password")
|
||||||
|
));
|
||||||
|
$this->loginRequired = true;
|
||||||
|
$this->forbidMethod("GET");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function _execute(): bool {
|
||||||
|
|
||||||
|
$currentUser = $this->context->getUser();
|
||||||
|
$gpgKey = $currentUser->getGPG();
|
||||||
|
if (!$gpgKey) {
|
||||||
|
return $this->createError("You have not added a GPG public key to your account yet.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = $this->context->getSQL();
|
||||||
|
$password = $this->getParam("password");
|
||||||
|
if (!password_verify($password, $currentUser->password)) {
|
||||||
|
return $this->createError("Incorrect password.");
|
||||||
|
} else if (!$gpgKey->delete($sql)) {
|
||||||
|
return $this->createError("Error deleting gpg key: " . $sql->getLastError());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getDefaultACL(Insert $insert): void {
|
||||||
|
$insert->addRow(self::getEndpoint(), [], "Allows users to unlink gpg keys from their profile", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Confirm extends GpgKeyAPI {
|
||||||
|
|
||||||
|
public function __construct(Context $context, bool $externalCall = false) {
|
||||||
|
parent::__construct($context, $externalCall, [
|
||||||
|
"token" => new StringType("token", 36)
|
||||||
|
]);
|
||||||
|
$this->loginRequired = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function _execute(): bool {
|
||||||
|
|
||||||
|
$currentUser = $this->context->getUser();
|
||||||
|
$gpgKey = $currentUser->getGPG();
|
||||||
|
if (!$gpgKey) {
|
||||||
|
return $this->createError("You have not added a GPG key yet.");
|
||||||
|
} else if ($gpgKey->isConfirmed()) {
|
||||||
|
return $this->createError("Your GPG key is already confirmed");
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $this->getParam("token");
|
||||||
|
$sql = $this->context->getSQL();
|
||||||
|
|
||||||
|
$userToken = UserToken::findBy(UserToken::createBuilder($sql, true)
|
||||||
|
->whereEq("token", $token)
|
||||||
|
->where(new Compare("valid_until", $sql->now(), ">="))
|
||||||
|
->whereEq("user_id", $currentUser->getId())
|
||||||
|
->whereEq("token_type", UserToken::TYPE_GPG_CONFIRM));
|
||||||
|
|
||||||
|
if ($userToken !== false) {
|
||||||
|
if ($userToken === null) {
|
||||||
|
return $this->createError("Invalid token");
|
||||||
|
} else {
|
||||||
|
if (!$gpgKey->confirm($sql)) {
|
||||||
|
return $this->createError("Error updating gpg key: " . $sql->getLastError());
|
||||||
|
}
|
||||||
|
|
||||||
|
$userToken->invalidate($sql);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return $this->createError("Error validating token: " . $sql->getLastError());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Download extends GpgKeyAPI {
|
||||||
|
public function __construct(Context $context, bool $externalCall = false) {
|
||||||
|
parent::__construct($context, $externalCall, array(
|
||||||
|
"id" => new Parameter("id", Parameter::TYPE_INT, true, null),
|
||||||
|
"format" => new StringType("format", 16, true, "ascii")
|
||||||
|
));
|
||||||
|
$this->loginRequired = true;
|
||||||
|
$this->csrfTokenRequired = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function _execute(): bool {
|
||||||
|
|
||||||
|
$allowedFormats = ["json", "ascii", "gpg"];
|
||||||
|
$format = $this->getParam("format");
|
||||||
|
if (!in_array($format, $allowedFormats)) {
|
||||||
|
return $this->getParam("Invalid requested format. Allowed formats: " . implode(",", $allowedFormats));
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentUser = $this->context->getUser();
|
||||||
|
$userId = $this->getParam("id");
|
||||||
|
if ($userId === null || $userId == $currentUser->getId()) {
|
||||||
|
$gpgKey = $currentUser->getGPG();
|
||||||
|
if (!$gpgKey) {
|
||||||
|
return $this->createError("You did not add a gpg key yet.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$email = $currentUser->getEmail();
|
||||||
|
} else {
|
||||||
|
$sql = $this->context->getSQL();
|
||||||
|
$user = User::find($sql, $userId, true);
|
||||||
|
if ($user === false) {
|
||||||
|
return $this->createError("Error fetching user details: " . $sql->getLastError());
|
||||||
|
} else if ($user === null) {
|
||||||
|
return $this->createError("User not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
$email = $user->getEmail();
|
||||||
|
$gpgKey = $user->getGPG();
|
||||||
|
if (!$gpgKey || !$gpgKey->isConfirmed()) {
|
||||||
|
return $this->createError("This user has not added a gpg key yet or has not confirmed it yet.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$res = GpgKey::export($gpgKey->getFingerprint(), $format !== "gpg");
|
||||||
|
if (!$res["success"]) {
|
||||||
|
return $this->createError($res["error"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = $res["data"];
|
||||||
|
if ($format === "json") {
|
||||||
|
$this->result["key"] = $key;
|
||||||
|
return true;
|
||||||
|
} else if ($format === "ascii") {
|
||||||
|
$contentType = "application/pgp-keys";
|
||||||
|
$ext = "asc";
|
||||||
|
} else if ($format === "gpg") {
|
||||||
|
$contentType = "application/octet-stream";
|
||||||
|
$ext = "gpg";
|
||||||
|
} else {
|
||||||
|
die("Invalid format");
|
||||||
|
}
|
||||||
|
|
||||||
|
$fileName = "$email.$ext";
|
||||||
|
header("Content-Type: $contentType");
|
||||||
|
header("Content-Length: " . strlen($key));
|
||||||
|
header("Content-Disposition: attachment; filename=\"$fileName\"");
|
||||||
|
die($key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -604,4 +604,12 @@ abstract class Request {
|
|||||||
$currentUser = $this->context->getUser();
|
$currentUser = $this->context->getUser();
|
||||||
return $currentUser ? "userId='" . $currentUser->getId() . "'" : "SYSTEM";
|
return $currentUser ? "userId='" . $currentUser->getId() . "'" : "SYSTEM";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function formatDuration(int $count, string $string): string {
|
||||||
|
if ($count === 1) {
|
||||||
|
return $string;
|
||||||
|
} else {
|
||||||
|
return "the next $count {$string}s";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -63,6 +63,9 @@ class Swagger extends Request {
|
|||||||
|
|
||||||
$definitions = [];
|
$definitions = [];
|
||||||
$paths = [];
|
$paths = [];
|
||||||
|
$tags = [];
|
||||||
|
|
||||||
|
// TODO: consumes and produces is not always the same, but it's okay for now
|
||||||
foreach (self::getApiEndpoints() as $endpoint => $apiClass) {
|
foreach (self::getApiEndpoints() as $endpoint => $apiClass) {
|
||||||
$body = null;
|
$body = null;
|
||||||
$requiredProperties = [];
|
$requiredProperties = [];
|
||||||
@ -72,6 +75,17 @@ class Swagger extends Request {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$tag = null;
|
||||||
|
if ($apiClass->getParentClass()->getName() !== Request::class) {
|
||||||
|
$parentClass = $apiClass->getParentClass()->getShortName();
|
||||||
|
if (endsWith($parentClass, "API")) {
|
||||||
|
$tag = substr($parentClass, 0, strlen($parentClass) - 3);
|
||||||
|
if (!in_array($tag, $tags)) {
|
||||||
|
$tags[] = $tag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$parameters = $apiObject->getDefaultParams();
|
$parameters = $apiObject->getDefaultParams();
|
||||||
if (!empty($parameters)) {
|
if (!empty($parameters)) {
|
||||||
$body = [];
|
$body = [];
|
||||||
@ -107,6 +121,7 @@ class Swagger extends Request {
|
|||||||
|
|
||||||
$endPointDefinition = [
|
$endPointDefinition = [
|
||||||
"post" => [
|
"post" => [
|
||||||
|
"tags" => [$tag ?? "Global"],
|
||||||
"produces" => ["application/json"],
|
"produces" => ["application/json"],
|
||||||
"responses" => [
|
"responses" => [
|
||||||
"200" => ["description" => "OK!"],
|
"200" => ["description" => "OK!"],
|
||||||
@ -123,7 +138,7 @@ class Swagger extends Request {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($body) {
|
if ($body) {
|
||||||
$endPointDefinition["post"]["consumes"] = ["application/json"];
|
$endPointDefinition["post"]["consumes"] = ["application/json", "application/x-www-form-urlencoded"];
|
||||||
$endPointDefinition["post"]["parameters"] = [[
|
$endPointDefinition["post"]["parameters"] = [[
|
||||||
"in" => "body",
|
"in" => "body",
|
||||||
"name" => "body",
|
"name" => "body",
|
||||||
@ -149,6 +164,7 @@ class Swagger extends Request {
|
|||||||
"host" => $domain,
|
"host" => $domain,
|
||||||
"basePath" => "/api",
|
"basePath" => "/api",
|
||||||
"schemes" => ["$protocol"],
|
"schemes" => ["$protocol"],
|
||||||
|
"tags" => $tags,
|
||||||
"paths" => $paths,
|
"paths" => $paths,
|
||||||
"definitions" => $definitions
|
"definitions" => $definitions
|
||||||
];
|
];
|
||||||
|
@ -98,14 +98,6 @@ namespace Core\API {
|
|||||||
return password_hash($password, PASSWORD_BCRYPT);
|
return password_hash($password, PASSWORD_BCRYPT);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function formatDuration(int $count, string $string): string {
|
|
||||||
if ($count === 1) {
|
|
||||||
return $string;
|
|
||||||
} else {
|
|
||||||
return "the next $count {$string}s";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function checkToken(string $token) : UserToken|bool {
|
protected function checkToken(string $token) : UserToken|bool {
|
||||||
$sql = $this->context->getSQL();
|
$sql = $this->context->getSQL();
|
||||||
$userToken = UserToken::findBy(UserToken::createBuilder($sql, true)
|
$userToken = UserToken::findBy(UserToken::createBuilder($sql, true)
|
||||||
@ -1244,270 +1236,6 @@ namespace Core\API\User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ImportGPG extends UserAPI {
|
|
||||||
|
|
||||||
public function __construct(Context $context, bool $externalCall = false) {
|
|
||||||
parent::__construct($context, $externalCall, [
|
|
||||||
"pubkey" => new StringType("pubkey")
|
|
||||||
]);
|
|
||||||
$this->loginRequired = true;
|
|
||||||
$this->forbidMethod("GET");
|
|
||||||
}
|
|
||||||
|
|
||||||
private function testKey(string $keyString) {
|
|
||||||
$res = GpgKey::getKeyInfo($keyString);
|
|
||||||
if (!$res["success"]) {
|
|
||||||
return $this->createError($res["error"] ?? $res["msg"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$keyData = $res["data"];
|
|
||||||
$keyType = $keyData["type"];
|
|
||||||
$expires = $keyData["expires"];
|
|
||||||
|
|
||||||
if ($keyType === "sec#") {
|
|
||||||
return self::createError("ATTENTION! It seems like you've imported a PGP PRIVATE KEY instead of a public key.
|
|
||||||
It is recommended to immediately revoke your private key and create a new key pair.");
|
|
||||||
} else if ($keyType !== "pub") {
|
|
||||||
return self::createError("Unknown key type: $keyType");
|
|
||||||
} else if (isInPast($expires)) {
|
|
||||||
return self::createError("It seems like the gpg key is already expired.");
|
|
||||||
} else {
|
|
||||||
return $keyData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function _execute(): bool {
|
|
||||||
|
|
||||||
$currentUser = $this->context->getUser();
|
|
||||||
$gpgKey = $currentUser->getGPG();
|
|
||||||
if ($gpgKey) {
|
|
||||||
return $this->createError("You already added a GPG key to your account.");
|
|
||||||
} else if (!$currentUser->getEmail()) {
|
|
||||||
return $this->createError("You do not have an e-mail address");
|
|
||||||
}
|
|
||||||
|
|
||||||
// fix key first, enforce a newline after
|
|
||||||
$keyString = $this->getParam("pubkey");
|
|
||||||
$keyString = preg_replace("/(-{2,})\n([^\n])/", "$1\n\n$2", $keyString);
|
|
||||||
$keyData = $this->testKey($keyString);
|
|
||||||
if ($keyData === false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$res = GpgKey::importKey($keyString);
|
|
||||||
if (!$res["success"]) {
|
|
||||||
return $this->createError($res["error"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$sql = $this->context->getSQL();
|
|
||||||
$gpgKey = new GpgKey($keyData["fingerprint"], $keyData["algorithm"], $keyData["expires"]);
|
|
||||||
if (!$gpgKey->save($sql)) {
|
|
||||||
return $this->createError("Error creating gpg key: " . $sql->getLastError());
|
|
||||||
}
|
|
||||||
|
|
||||||
$token = generateRandomString(36);
|
|
||||||
$userToken = new UserToken($currentUser, $token, UserToken::TYPE_GPG_CONFIRM, 1);
|
|
||||||
if (!$userToken->save($sql)) {
|
|
||||||
return $this->createError("Error saving user token: " . $sql->getLastError());
|
|
||||||
}
|
|
||||||
|
|
||||||
$validHours = 1;
|
|
||||||
$settings = $this->context->getSettings();
|
|
||||||
$baseUrl = $settings->getBaseUrl();
|
|
||||||
$siteName = $settings->getSiteName();
|
|
||||||
$req = new Render($this->context);
|
|
||||||
$this->success = $req->execute([
|
|
||||||
"file" => "mail/gpg_import.twig",
|
|
||||||
"parameters" => [
|
|
||||||
"link" => "$baseUrl/resetPassword?token=$token",
|
|
||||||
"site_name" => $siteName,
|
|
||||||
"base_url" => $baseUrl,
|
|
||||||
"username" => $currentUser->getDisplayName(),
|
|
||||||
"valid_time" => $this->formatDuration($validHours, "hour")
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->lastError = $req->getLastError();
|
|
||||||
|
|
||||||
if ($this->success) {
|
|
||||||
$messageBody = $req->getResult()["html"];
|
|
||||||
$sendMail = new \Core\API\Mail\Send($this->context);
|
|
||||||
$this->success = $sendMail->execute(array(
|
|
||||||
"to" => $currentUser->getEmail(),
|
|
||||||
"subject" => "[$siteName] Confirm GPG-Key",
|
|
||||||
"body" => $messageBody,
|
|
||||||
"gpgFingerprint" => $gpgKey->getFingerprint()
|
|
||||||
));
|
|
||||||
|
|
||||||
$this->lastError = $sendMail->getLastError();
|
|
||||||
|
|
||||||
if ($this->success) {
|
|
||||||
$currentUser->gpgKey = $gpgKey;
|
|
||||||
if ($currentUser->save($sql, ["gpgKey"])) {
|
|
||||||
$this->result["gpgKey"] = $gpgKey->jsonSerialize();
|
|
||||||
} else {
|
|
||||||
return $this->createError("Error updating user details: " . $sql->getLastError());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getDefaultACL(Insert $insert): void {
|
|
||||||
$insert->addRow(self::getEndpoint(), [], "Allows users to import gpg keys for a secure e-mail communication", true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RemoveGPG extends UserAPI {
|
|
||||||
public function __construct(Context $context, bool $externalCall = false) {
|
|
||||||
parent::__construct($context, $externalCall, array(
|
|
||||||
"password" => new StringType("password")
|
|
||||||
));
|
|
||||||
$this->loginRequired = true;
|
|
||||||
$this->forbidMethod("GET");
|
|
||||||
}
|
|
||||||
|
|
||||||
public function _execute(): bool {
|
|
||||||
|
|
||||||
$currentUser = $this->context->getUser();
|
|
||||||
$gpgKey = $currentUser->getGPG();
|
|
||||||
if (!$gpgKey) {
|
|
||||||
return $this->createError("You have not added a GPG public key to your account yet.");
|
|
||||||
}
|
|
||||||
|
|
||||||
$sql = $this->context->getSQL();
|
|
||||||
$password = $this->getParam("password");
|
|
||||||
if (!password_verify($password, $currentUser->password)) {
|
|
||||||
return $this->createError("Incorrect password.");
|
|
||||||
} else if (!$gpgKey->delete($sql)) {
|
|
||||||
return $this->createError("Error deleting gpg key: " . $sql->getLastError());
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getDefaultACL(Insert $insert): void {
|
|
||||||
$insert->addRow(self::getEndpoint(), [], "Allows users to unlink gpg keys from their profile", true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ConfirmGPG extends UserAPI {
|
|
||||||
|
|
||||||
public function __construct(Context $context, bool $externalCall = false) {
|
|
||||||
parent::__construct($context, $externalCall, [
|
|
||||||
"token" => new StringType("token", 36)
|
|
||||||
]);
|
|
||||||
$this->loginRequired = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function _execute(): bool {
|
|
||||||
|
|
||||||
$currentUser = $this->context->getUser();
|
|
||||||
$gpgKey = $currentUser->getGPG();
|
|
||||||
if (!$gpgKey) {
|
|
||||||
return $this->createError("You have not added a GPG key yet.");
|
|
||||||
} else if ($gpgKey->isConfirmed()) {
|
|
||||||
return $this->createError("Your GPG key is already confirmed");
|
|
||||||
}
|
|
||||||
|
|
||||||
$token = $this->getParam("token");
|
|
||||||
$sql = $this->context->getSQL();
|
|
||||||
|
|
||||||
$userToken = UserToken::findBy(UserToken::createBuilder($sql, true)
|
|
||||||
->whereEq("token", $token)
|
|
||||||
->where(new Compare("valid_until", $sql->now(), ">="))
|
|
||||||
->whereEq("user_id", $currentUser->getId())
|
|
||||||
->whereEq("token_type", UserToken::TYPE_GPG_CONFIRM));
|
|
||||||
|
|
||||||
if ($userToken !== false) {
|
|
||||||
if ($userToken === null) {
|
|
||||||
return $this->createError("Invalid token");
|
|
||||||
} else {
|
|
||||||
if (!$gpgKey->confirm($sql)) {
|
|
||||||
return $this->createError("Error updating gpg key: " . $sql->getLastError());
|
|
||||||
}
|
|
||||||
|
|
||||||
$userToken->invalidate($sql);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return $this->createError("Error validating token: " . $sql->getLastError());
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DownloadGPG extends UserAPI {
|
|
||||||
public function __construct(Context $context, bool $externalCall = false) {
|
|
||||||
parent::__construct($context, $externalCall, array(
|
|
||||||
"id" => new Parameter("id", Parameter::TYPE_INT, true, null),
|
|
||||||
"format" => new StringType("format", 16, true, "ascii")
|
|
||||||
));
|
|
||||||
$this->loginRequired = true;
|
|
||||||
$this->csrfTokenRequired = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function _execute(): bool {
|
|
||||||
|
|
||||||
$allowedFormats = ["json", "ascii", "gpg"];
|
|
||||||
$format = $this->getParam("format");
|
|
||||||
if (!in_array($format, $allowedFormats)) {
|
|
||||||
return $this->getParam("Invalid requested format. Allowed formats: " . implode(",", $allowedFormats));
|
|
||||||
}
|
|
||||||
|
|
||||||
$currentUser = $this->context->getUser();
|
|
||||||
$userId = $this->getParam("id");
|
|
||||||
if ($userId === null || $userId == $currentUser->getId()) {
|
|
||||||
$gpgKey = $currentUser->getGPG();
|
|
||||||
if (!$gpgKey) {
|
|
||||||
return $this->createError("You did not add a gpg key yet.");
|
|
||||||
}
|
|
||||||
|
|
||||||
$email = $currentUser->getEmail();
|
|
||||||
} else {
|
|
||||||
$sql = $this->context->getSQL();
|
|
||||||
$user = User::find($sql, $userId, true);
|
|
||||||
if ($user === false) {
|
|
||||||
return $this->createError("Error fetching user details: " . $sql->getLastError());
|
|
||||||
} else if ($user === null) {
|
|
||||||
return $this->createError("User not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
$email = $user->getEmail();
|
|
||||||
$gpgKey = $user->getGPG();
|
|
||||||
if (!$gpgKey || !$gpgKey->isConfirmed()) {
|
|
||||||
return $this->createError("This user has not added a gpg key yet or has not confirmed it yet.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$res = GpgKey::export($gpgKey->getFingerprint(), $format !== "gpg");
|
|
||||||
if (!$res["success"]) {
|
|
||||||
return $this->createError($res["error"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$key = $res["data"];
|
|
||||||
if ($format === "json") {
|
|
||||||
$this->result["key"] = $key;
|
|
||||||
return true;
|
|
||||||
} else if ($format === "ascii") {
|
|
||||||
$contentType = "application/pgp-keys";
|
|
||||||
$ext = "asc";
|
|
||||||
} else if ($format === "gpg") {
|
|
||||||
$contentType = "application/octet-stream";
|
|
||||||
$ext = "gpg";
|
|
||||||
} else {
|
|
||||||
die("Invalid format");
|
|
||||||
}
|
|
||||||
|
|
||||||
$fileName = "$email.$ext";
|
|
||||||
header("Content-Type: $contentType");
|
|
||||||
header("Content-Length: " . strlen($key));
|
|
||||||
header("Content-Disposition: attachment; filename=\"$fileName\"");
|
|
||||||
die($key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class UploadPicture extends UserAPI {
|
class UploadPicture extends UserAPI {
|
||||||
public function __construct(Context $context, bool $externalCall = false) {
|
public function __construct(Context $context, bool $externalCall = false) {
|
||||||
// TODO: we should optimize the process here, we need an offset and size parameter to get a quadratic crop of the uploaded image
|
// TODO: we should optimize the process here, we need an offset and size parameter to get a quadratic crop of the uploaded image
|
||||||
|
@ -45,6 +45,7 @@ class Security extends Document {
|
|||||||
"# This project is based on the open-source framework hosted on https://github.com/rhergenreder/web-base",
|
"# This project is based on the open-source framework hosted on https://github.com/rhergenreder/web-base",
|
||||||
"# Any non site-specific issues can be reported via the github security reporting feature:",
|
"# Any non site-specific issues can be reported via the github security reporting feature:",
|
||||||
"# https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability",
|
"# https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability",
|
||||||
|
"# or by contacting me directly: mail(at)romanh(dot)de",
|
||||||
"",
|
"",
|
||||||
"Canonical: $baseUrl/.well-known/security.txt",
|
"Canonical: $baseUrl/.well-known/security.txt",
|
||||||
"Preferred-Languages: $languageCodes",
|
"Preferred-Languages: $languageCodes",
|
||||||
|
@ -62,7 +62,7 @@ class ApiRoute extends Route {
|
|||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
$response = createError("Invalid Method");
|
$response = createError("Invalid Method");
|
||||||
} else {
|
} else {
|
||||||
$request = $apiClass->newInstanceArgs(array($router->getContext(), true));
|
$request = $apiClass->newInstanceArgs([$router->getContext(), true]);
|
||||||
$success = $request->execute();
|
$success = $request->execute();
|
||||||
$response = $request->getResult();
|
$response = $request->getResult();
|
||||||
$response["success"] = $success;
|
$response["success"] = $success;
|
||||||
@ -74,6 +74,7 @@ class ApiRoute extends Route {
|
|||||||
}
|
}
|
||||||
} catch (ReflectionException $e) {
|
} catch (ReflectionException $e) {
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
|
$router->getLogger()->error("Error instantiating class: $e");
|
||||||
$response = createError("Error instantiating class: $e");
|
$response = createError("Error instantiating class: $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,6 +83,7 @@ class DocumentRoute extends Route {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (!$this->loadClass()) {
|
if (!$this->loadClass()) {
|
||||||
|
$router->getLogger()->warning("Error loading class: $className");
|
||||||
return $router->returnStatusCode(500, [ "message" => "Error loading class: $className"]);
|
return $router->returnStatusCode(500, [ "message" => "Error loading class: $className"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,6 +91,7 @@ class DocumentRoute extends Route {
|
|||||||
$document = $this->reflectionClass->newInstanceArgs($args);
|
$document = $this->reflectionClass->newInstanceArgs($args);
|
||||||
return $document->load($params);
|
return $document->load($params);
|
||||||
} catch (\ReflectionException $e) {
|
} catch (\ReflectionException $e) {
|
||||||
|
$router->getLogger()->error("Error loading class: $className: " . $e->getMessage());
|
||||||
return $router->returnStatusCode(500, [ "message" => "Error loading class $className: " . $e->getMessage()]);
|
return $router->returnStatusCode(500, [ "message" => "Error loading class $className: " . $e->getMessage()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
let token = jsCore.getParameter("token");
|
let token = jsCore.getParameter("token");
|
||||||
let confirmStatus = $("#confirm-status");
|
let confirmStatus = $("#confirm-status");
|
||||||
if (token) {
|
if (token) {
|
||||||
jsCore.apiCall("/user/confirmGPG", { token: token, csrfToken: '{{ user.session.csrfToken }}' }, (res) => {
|
jsCore.apiCall("/gpgKey/confirm", { token: token, csrfToken: '{{ user.session.csrfToken }}' }, (res) => {
|
||||||
confirmStatus.removeClass("alert-info");
|
confirmStatus.removeClass("alert-info");
|
||||||
if (!res.success) {
|
if (!res.success) {
|
||||||
confirmStatus.addClass("alert-danger");
|
confirmStatus.addClass("alert-danger");
|
||||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -126,7 +126,7 @@ export default function GpgBox(props) {
|
|||||||
<Button startIcon={isGpgKeyRemoving ? <CircularProgress size={12} /> : <Remove />}
|
<Button startIcon={isGpgKeyRemoving ? <CircularProgress size={12} /> : <Remove />}
|
||||||
color="secondary" onClick={onRemoveGpgKey}
|
color="secondary" onClick={onRemoveGpgKey}
|
||||||
variant="outlined" size="small"
|
variant="outlined" size="small"
|
||||||
disabled={isGpgKeyRemoving || !api.hasPermission("user/removeGPG")}>
|
disabled={isGpgKeyRemoving || !api.hasPermission("gpgKey/remove")}>
|
||||||
{isGpgKeyRemoving ? L("general.removing") + "…" : L("general.remove")}
|
{isGpgKeyRemoving ? L("general.removing") + "…" : L("general.remove")}
|
||||||
</Button>
|
</Button>
|
||||||
</Box> :
|
</Box> :
|
||||||
@ -134,7 +134,7 @@ export default function GpgBox(props) {
|
|||||||
<SpacedFormGroup>
|
<SpacedFormGroup>
|
||||||
<FormLabel>{L("account.gpg_key")}</FormLabel>
|
<FormLabel>{L("account.gpg_key")}</FormLabel>
|
||||||
<GpgKeyField value={gpgKey} multiline={true} rows={8}
|
<GpgKeyField value={gpgKey} multiline={true} rows={8}
|
||||||
disabled={isGpgKeyUploading || !api.hasPermission("user/importGPG")}
|
disabled={isGpgKeyUploading || !api.hasPermission("gpgKey/import")}
|
||||||
placeholder={L("account.gpg_key_placeholder_text")}
|
placeholder={L("account.gpg_key_placeholder_text")}
|
||||||
onChange={e => setGpgKey(e.target.value)}
|
onChange={e => setGpgKey(e.target.value)}
|
||||||
onDrop={e => {
|
onDrop={e => {
|
||||||
@ -162,7 +162,7 @@ export default function GpgBox(props) {
|
|||||||
<Button startIcon={isGpgKeyUploading ? <CircularProgress size={12} /> : <Upload />}
|
<Button startIcon={isGpgKeyUploading ? <CircularProgress size={12} /> : <Upload />}
|
||||||
color="primary" onClick={onUploadGPG}
|
color="primary" onClick={onUploadGPG}
|
||||||
variant="outlined" size="small"
|
variant="outlined" size="small"
|
||||||
disabled={isGpgKeyUploading || !api.hasPermission("user/importGPG")}>
|
disabled={isGpgKeyUploading || !api.hasPermission("gpgKey/import")}>
|
||||||
{isGpgKeyUploading ? L("general.uploading") + "…" : L("general.upload")}
|
{isGpgKeyUploading ? L("general.uploading") + "…" : L("general.upload")}
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonBar>
|
</ButtonBar>
|
||||||
|
@ -387,7 +387,7 @@ export default class API {
|
|||||||
|
|
||||||
/** GPG API **/
|
/** GPG API **/
|
||||||
async uploadGPG(pubkey) {
|
async uploadGPG(pubkey) {
|
||||||
let res = await this.apiCall("user/importGPG", { pubkey: pubkey });
|
let res = await this.apiCall("gpgKey/import", { pubkey: pubkey });
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
this.user.gpgKey = res.gpgKey;
|
this.user.gpgKey = res.gpgKey;
|
||||||
}
|
}
|
||||||
@ -396,7 +396,7 @@ export default class API {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async confirmGpgToken(token) {
|
async confirmGpgToken(token) {
|
||||||
let res = await this.apiCall("user/confirmGPG", { token: token });
|
let res = await this.apiCall("gpgKey/confirm", { token: token });
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
this.user.gpgKey.confirmed = true;
|
this.user.gpgKey.confirmed = true;
|
||||||
}
|
}
|
||||||
@ -405,7 +405,7 @@ export default class API {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async removeGPG(password) {
|
async removeGPG(password) {
|
||||||
let res = await this.apiCall("user/removeGPG", { password: password });
|
let res = await this.apiCall("gpgKey/remove", { password: password });
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
this.user.gpgKey = null;
|
this.user.gpgKey = null;
|
||||||
}
|
}
|
||||||
@ -414,7 +414,7 @@ export default class API {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async downloadGPG(userId) {
|
async downloadGPG(userId) {
|
||||||
return this.apiCall("user/downloadGPG", { id: userId }, true);
|
return this.apiCall("gpgKey/download", { id: userId }, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Log API **/
|
/** Log API **/
|
||||||
|
Loading…
Reference in New Issue
Block a user