Settings + Test Mail

This commit is contained in:
Roman committed 2020-06-26 14:58:17 +02:00
1 parent 6eb9bf333f
commit 09475be545
11 files changed
+528 -104

No files matched your search

+5 -10
View File
@@ -11,12 +11,9 @@ class SendMail extends Request {
public function __construct($user, $externalCall = false) {
parent::__construct($user, $externalCall, array(
'from' => new Parameter('from', Parameter::TYPE_EMAIL),
'to' => new Parameter('to', Parameter::TYPE_EMAIL),
'subject' => new StringType('subject', -1),
'body' => new StringType('body', -1),
'fromName' => new StringType('fromName', -1, true, ''),
'replyTo' => new Parameter('to', Parameter::TYPE_EMAIL, true, ''),
));
$this->isPublic = false;
}
@@ -28,6 +25,7 @@ class SendMail extends Request {
if ($this->success) {
$settings = $req->getResult()["settings"];
if (!isset($settings["mail_enabled"]) || $settings["mail_enabled"] !== "1") {
$this->createError("Mail is not configured yet.");
return null;
@@ -37,7 +35,9 @@ class SendMail extends Request {
$port = intval($settings["mail_port"] ?? "25");
$login = $settings["mail_username"] ?? "";
$password = $settings["mail_password"] ?? "";
return new ConnectionData($host, $port, $login, $password);
$connectionData = new ConnectionData($host, $port, $login, $password);
$connectionData->setProperty("from", $settings["mail_from"] ?? "");
return $connectionData;
}
return null;
@@ -56,7 +56,7 @@ class SendMail extends Request {
try {
$mail = new PHPMailer;
$mail->IsSMTP();
$mail->setFrom($this->getParam('from'), $this->getParam('fromName'));
$mail->setFrom($mailConfig->getProperty("from"));
$mail->addAddress($this->getParam('to'));
$mail->Subject = $this->getParam('subject');
$mail->SMTPDebug = 0;
@@ -70,11 +70,6 @@ class SendMail extends Request {
$mail->CharSet = 'UTF-8';
$mail->Body = $this->getParam('body');
$replyTo = $this->getParam('replyTo');
if(!is_null($replyTo) && !empty($replyTo)) {
$mail->AddReplyTo($replyTo, $this->getParam('fromName'));
}
$this->success = @$mail->Send();
if (!$this->success) {
$this->lastError = "Error sending Mail: $mail->ErrorInfo";
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace Api;
use Api\Parameter\Parameter;
use Objects\User;
class SendTestMail extends Request {
public function __construct(User $user, bool $externalCall = false) {
parent::__construct($user, $externalCall, array(
"receiver" => new Parameter("receiver", Parameter::TYPE_EMAIL)
));
}
public function execute($values = array()) {
if (!parent::execute($values)) {
return false;
}
$receiver = $this->getParam("receiver");
$req = new SendMail($this->user);
$this->success = $req->execute(array(
"to" => $receiver,
"subject" => "Test E-Mail",
"body" => "Hey! If you receive this e-mail, your mail configuration seems to be working."
));
$this->lastError = $req->getLastError();
return $this->success;
}
}
+4 -2
View File
@@ -16,6 +16,7 @@ namespace Api\Settings {
use Driver\SQL\Column\Column;
use Driver\SQL\Condition\Compare;
use Driver\SQL\Condition\CondLike;
use Driver\SQL\Condition\CondNot;
use Driver\SQL\Condition\CondRegex;
use Driver\SQL\Strategy\UpdateStrategy;
use Objects\User;
@@ -42,11 +43,12 @@ namespace Api\Settings {
$query = $sql->select("name", "value") ->from("Settings");
if (!is_null($key) && !empty($key)) {
$query->where(new CondRegex($key, new Column("name")));
$query->where(new CondRegex(new Column("name"), $key));
}
// filter sensitive values, if called from outside
if ($this->isExternalCall()) {
$query->where(new Compare("name", "jwt_secret", "!="));
$query->where(new CondNot("private"));
}
$res = $query->execute();
+2 -1
View File
@@ -461,7 +461,8 @@ If the invitation was not intended, you can simply ignore this email.<br><br><a
return $this->createError("Error creating Session: " . $sql->getLastError());
} else {
$this->result["loggedIn"] = true;
$this->result['logoutIn'] = $this->user->getSession()->getExpiresSeconds();
$this->result["logoutIn"] = $this->user->getSession()->getExpiresSeconds();
$this->result["csrf_token"] = $this->user->getSession()->getCsrfToken();
$this->success = true;
}
} else {
+46 -6
View File
@@ -138,24 +138,64 @@ class CreateDatabase {
->addRow("^/register(/)?$", "dynamic", "\\Documents\\Account", "\\Views\\Account\\Register")
->addRow("^/confirmEmail(/)?$", "dynamic", "\\Documents\\Account", "\\Views\\Account\\ConfirmEmail")
->addRow("^/acceptInvite(/)?$", "dynamic", "\\Documents\\Account", "\\Views\\Account\\AcceptInvite")
->addRow("^/resetPassword(/)?$", "dynamic", "\\Documents\\Account", "\\Views\\Account\\ResetPassword")
->addRow("^/$", "static", "/static/welcome.html", NULL);
$queries[] = $sql->createTable("Settings")
->addString("name", 32)
->addString("value", 1024, true)
->addBool("private", false)
->primaryKey("name");
$settingsQuery = $sql->insert("Settings", array("name", "value"))
$settingsQuery = $sql->insert("Settings", array("name", "value", "private"))
// ->addRow("mail_enabled", "0") # this key will be set during installation
->addRow("mail_host", "")
->addRow("mail_port", "")
->addRow("mail_username", "")
->addRow("mail_password", "")
->addRow("mail_from", "");
->addRow("mail_host", "", false)
->addRow("mail_port", "", false)
->addRow("mail_username", "", false)
->addRow("mail_password", "", true)
->addRow("mail_from", "", false)
->addRow("message_confirm_email", self::MessageConfirmEmail(), false)
->addRow("message_accept_invite", self::MessageAcceptInvite(), false)
->addRow("message_reset_password", self::MessageResetPassword(), false);
(Settings::loadDefaults())->addRows($settingsQuery);
$queries[] = $settingsQuery;
return $queries;
}
private static function MessageConfirmEmail() : string {
return str_replace("\n", "", intendCode(
"Hello {{username}},<br>
You recently created an account on {{site_name}}. Please click on the following link to
confirm your email address and complete your registration. If you haven't registered an
account, you can simply ignore this email. The link is valid for the next 48 hours:<br><br>
<a href=\"{{link}}\">{{confirm_link}}</a><br><br>
Best Regards<br>
{{site_name}} Administration", false
));
}
private static function MessageAcceptInvite() : string {
return str_replace("\n", "", intendCode(
"Hello {{username}},<br>
You were invited to create an account on {{site_name}}. Please click on the following link to
confirm your email address and complete your registration by choosing a new password.
If you want to decline the invitation, you can simply ignore this email. The link is valid for the next 48 hours:<br><br>
<a href=\"{{link}}\">{{link}}</a><br><br>
Best Regards<br>
{{site_name}} Administration", false
));
}
private static function MessageResetPassword() : string {
return str_replace("\n", "", intendCode(
"Hello {{username}},<br>
you requested a password reset on {{sitename}}. Please click on the following link to
choose a new password. If this request was not intended, you can simply ignore the email. The Link is valid for one hour:<br><br>
<a href=\"{{link}}\">{{link}}</a><br><br>
Best Regards<br>
{{site_name}} Administration", false
));
}
}
+5 -5
View File
@@ -62,10 +62,10 @@ class Settings {
}
public function addRows(Insert $query) {
$query->addRow("site_name", $this->siteName)
->addRow("base_url", $this->baseUrl)
->addRow("user_registration_enabled", $this->registrationAllowed ? "1" : "0")
->addRow("installation_completed", $this->installationComplete ? "1" : "0")
->addRow("jwt_secret", $this->jwtSecret);
$query->addRow("site_name", $this->siteName, false)
->addRow("base_url", $this->baseUrl, false)
->addRow("user_registration_enabled", $this->registrationAllowed ? "1" : "0", false)
->addRow("installation_completed", $this->installationComplete ? "1" : "0", true)
->addRow("jwt_secret", $this->jwtSecret, true);
}
}
@@ -0,0 +1,16 @@
<?php
namespace Driver\SQL\Condition;
class CondNot extends Condition {
private $expression; // string or condition
public function __construct($expression) {
$this->expression = $expression;
}
public function getExpression() {
return $this->expression;
}
}
+14
View File
@@ -6,7 +6,9 @@ use Driver\SQL\Column\Column;
use Driver\SQL\Condition\Compare;
use Driver\SQL\Condition\CondBool;
use Driver\SQL\Condition\CondIn;
use Driver\SQL\Condition\Condition;
use Driver\SQL\Condition\CondKeyword;
use Driver\SQL\Condition\CondNot;
use Driver\SQL\Condition\CondOr;
use Driver\SQL\Condition\CondRegex;
use Driver\SQL\Constraint\Constraint;
@@ -339,6 +341,9 @@ abstract class SQL {
return implode(" AND ", $conditions);
}
} else if($condition instanceof CondIn) {
$value = $condition->getValues();
$values = array();
foreach ($condition->getValues() as $value) {
$values[] = $this->addValue($value, $params);
@@ -353,6 +358,15 @@ abstract class SQL {
$left = ($left instanceof Column) ? $this->columnName($left->getName()) : $this->addValue($left, $params);
$right = ($right instanceof Column) ? $this->columnName($right->getName()) : $this->addValue($right, $params);
return "$left $keyword $right ";
} else if($condition instanceof CondNot) {
$expression = $condition->getExpression();
if ($expression instanceof Condition) {
$expression = $this->buildCondition($expression, $params);
} else {
$expression = $this->columnName($expression);
}
return "NOT $expression";
} else {
$this->lastError = "Unsupported condition type: " . get_class($condition);
return false;
+2 -2
View File
@@ -9110,7 +9110,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) *
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return API; });\n/* harmony import */ var babel_polyfill__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! babel-polyfill */ \"./node_modules/babel-polyfill/lib/index.js\");\n/* harmony import */ var babel_polyfill__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(babel_polyfill__WEBPACK_IMPORTED_MODULE_0__);\nfunction asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }\n\nfunction _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, \"next\", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, \"throw\", err); } _next(undefined); }); }; }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\n\n\nvar API = /*#__PURE__*/function () {\n function API() {\n _classCallCheck(this, API);\n\n this.loggedIn = false;\n this.user = {};\n }\n\n _createClass(API, [{\n key: \"csrfToken\",\n value: function csrfToken() {\n return this.loggedIn ? this.user.session.csrf_token : null;\n }\n }, {\n key: \"apiCall\",\n value: function () {\n var _apiCall = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(method, params) {\n var response, res;\n return regeneratorRuntime.wrap(function _callee$(_context) {\n while (1) {\n switch (_context.prev = _context.next) {\n case 0:\n params = params || {};\n params.csrf_token = this.csrfToken();\n _context.next = 4;\n return fetch(\"/api/\" + method, {\n method: 'post',\n headers: {\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify(params)\n });\n\n case 4:\n response = _context.sent;\n _context.next = 7;\n return response.json();\n\n case 7:\n res = _context.sent;\n\n if (!res.success && res.msg === \"You are not logged in.\") {\n document.location.reload();\n }\n\n return _context.abrupt(\"return\", res);\n\n case 10:\n case \"end\":\n return _context.stop();\n }\n }\n }, _callee, this);\n }));\n\n function apiCall(_x, _x2) {\n return _apiCall.apply(this, arguments);\n }\n\n return apiCall;\n }()\n }, {\n key: \"fetchUser\",\n value: function () {\n var _fetchUser = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee2() {\n var response, data;\n return regeneratorRuntime.wrap(function _callee2$(_context2) {\n while (1) {\n switch (_context2.prev = _context2.next) {\n case 0:\n _context2.next = 2;\n return fetch(\"/api/user/info\");\n\n case 2:\n response = _context2.sent;\n _context2.next = 5;\n return response.json();\n\n case 5:\n data = _context2.sent;\n this.user = data[\"user\"];\n this.loggedIn = data[\"loggedIn\"];\n return _context2.abrupt(\"return\", data && data.success && data.loggedIn);\n\n case 9:\n case \"end\":\n return _context2.stop();\n }\n }\n }, _callee2, this);\n }));\n\n function fetchUser() {\n return _fetchUser.apply(this, arguments);\n }\n\n return fetchUser;\n }()\n }, {\n key: \"editUser\",\n value: function () {\n var _editUser = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee3(id, username, email, password, groups) {\n returnLine truncated
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return API; });\n/* harmony import */ var babel_polyfill__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! babel-polyfill */ \"./node_modules/babel-polyfill/lib/index.js\");\n/* harmony import */ var babel_polyfill__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(babel_polyfill__WEBPACK_IMPORTED_MODULE_0__);\nfunction asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }\n\nfunction _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, \"next\", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, \"throw\", err); } _next(undefined); }); }; }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\n\n\nvar API = /*#__PURE__*/function () {\n function API() {\n _classCallCheck(this, API);\n\n this.loggedIn = false;\n this.user = {};\n }\n\n _createClass(API, [{\n key: \"csrfToken\",\n value: function csrfToken() {\n return this.loggedIn ? this.user.session.csrf_token : null;\n }\n }, {\n key: \"apiCall\",\n value: function () {\n var _apiCall = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(method, params) {\n var response, res;\n return regeneratorRuntime.wrap(function _callee$(_context) {\n while (1) {\n switch (_context.prev = _context.next) {\n case 0:\n params = params || {};\n params.csrf_token = this.csrfToken();\n _context.next = 4;\n return fetch(\"/api/\" + method, {\n method: 'post',\n headers: {\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify(params)\n });\n\n case 4:\n response = _context.sent;\n _context.next = 7;\n return response.json();\n\n case 7:\n res = _context.sent;\n\n if (!res.success && res.msg === \"You are not logged in.\") {\n document.location.reload();\n }\n\n return _context.abrupt(\"return\", res);\n\n case 10:\n case \"end\":\n return _context.stop();\n }\n }\n }, _callee, this);\n }));\n\n function apiCall(_x, _x2) {\n return _apiCall.apply(this, arguments);\n }\n\n return apiCall;\n }()\n }, {\n key: \"fetchUser\",\n value: function () {\n var _fetchUser = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee2() {\n var response, data;\n return regeneratorRuntime.wrap(function _callee2$(_context2) {\n while (1) {\n switch (_context2.prev = _context2.next) {\n case 0:\n _context2.next = 2;\n return fetch(\"/api/user/info\");\n\n case 2:\n response = _context2.sent;\n _context2.next = 5;\n return response.json();\n\n case 5:\n data = _context2.sent;\n this.user = data[\"user\"];\n this.loggedIn = data[\"loggedIn\"];\n return _context2.abrupt(\"return\", data && data.success && data.loggedIn);\n\n case 9:\n case \"end\":\n return _context2.stop();\n }\n }\n }, _callee2, this);\n }));\n\n function fetchUser() {\n return _fetchUser.apply(this, arguments);\n }\n\n return fetchUser;\n }()\n }, {\n key: \"editUser\",\n value: function () {\n var _editUser = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee3(id, username, email, password, groups) {\n returnLine truncated
/***/ }),
@@ -9335,7 +9335,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) *
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return Settings; });\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ \"./node_modules/react/index.js\");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var react_router_dom__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! react-router-dom */ \"./node_modules/react-router-dom/esm/react-router-dom.js\");\n/* harmony import */ var _elements_alert__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../elements/alert */ \"./src/elements/alert.js\");\n/* harmony import */ var react_collapse_lib_Collapse__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! react-collapse/lib/Collapse */ \"./node_modules/react-collapse/lib/Collapse.js\");\n/* harmony import */ var react_collapse_lib_Collapse__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(react_collapse_lib_Collapse__WEBPACK_IMPORTED_MODULE_3__);\n/* harmony import */ var _elements_icon__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../elements/icon */ \"./src/elements/icon.js\");\nfunction _typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return _typeof(obj); }\n\nfunction _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }\n\nfunction ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }\n\nfunction _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }\n\nfunction _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== \"function\" && superClass !== null) { throw new TypeError(\"Super expression must either be null or a function\"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }\n\nfunction _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }\n\nfunction _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }\n\nfunction _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === \"object\" || typeof call === \"function\")) { return call; } return _assertThisInitialized(self); }\n\nfunctionLine truncated
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return Settings; });\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ \"./node_modules/react/index.js\");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var react_router_dom__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! react-router-dom */ \"./node_modules/react-router-dom/esm/react-router-dom.js\");\n/* harmony import */ var _elements_alert__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../elements/alert */ \"./src/elements/alert.js\");\n/* harmony import */ var react_collapse_lib_Collapse__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! react-collapse/lib/Collapse */ \"./node_modules/react-collapse/lib/Collapse.js\");\n/* harmony import */ var react_collapse_lib_Collapse__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(react_collapse_lib_Collapse__WEBPACK_IMPORTED_MODULE_3__);\n/* harmony import */ var _elements_icon__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../elements/icon */ \"./src/elements/icon.js\");\nfunction _typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return _typeof(obj); }\n\nfunction _createForOfIteratorHelper(o, allowArrayLike) { var it; if (typeof Symbol === \"undefined\" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === \"number\") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError(\"Invalid attempt to iterate non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\"); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = o[Symbol.iterator](); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }\n\nfunction _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === \"string\") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === \"Object\" && o.constructor) n = o.constructor.name; if (n === \"Map\" || n === \"Set\") return Array.from(o); if (n === \"Arguments\" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }\n\nfunction _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }\n\nfunction _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }\n\nfunction ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }\n\nfunction _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }\n\nfunction _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; Line truncated
/***/ }),
+9
View File
@@ -94,4 +94,13 @@ export default class API {
async getSettings(key = "") {
return this.apiCall("settings/get", { key: key });
}
async saveSettings(settings) {
return this.apiCall("settings/set", { settings: settings });
}
async sendTestMail(receiver) {
return this.apiCall("sendTestMail", { receiver: receiver });
}
};
+392 -78
View File
@@ -11,15 +11,26 @@ export default class Settings extends React.Component {
this.state = {
errors: [],
mailErrors: [],
generalErrors: [],
etcErrors: [],
settings: {},
generalOpened: true,
mailOpened: true,
etcOpened : true
etcOpened: true,
isResetting: false,
isSaving: false,
isSending: false,
test_email: "",
unsavedMailSettings: false
};
this.parent = {
api: props.api
}
};
this.mailKeys = ["mail_enabled", "mail_host", "mail_port", "mail_username", "mail_password", "mail_from"];
this.generalKeys = ["site_name", "base_url", "user_registration_enabled"];
}
componentDidMount() {
@@ -34,11 +45,11 @@ export default class Settings extends React.Component {
});
}
removeError(i) {
if (i >= 0 && i < this.state.errors.length) {
let errors = this.state.errors.slice();
removeError(key, i) {
if (i >= 0 && i < this.state[key].length) {
let errors = this.state[key].slice();
errors.splice(i, 1);
this.setState({...this.state, errors: errors});
this.setState({...this.state, [key]: errors});
}
}
@@ -46,11 +57,306 @@ export default class Settings extends React.Component {
this.setState({...this.state, [key]: !this.state[key]});
}
getGeneralCard() {
let errors = [];
for (let i = 0; i < this.state.generalErrors.length; i++) {
errors.push(<Alert key={"error-" + i} onClose={() => this.removeError("generalErrors", i)} {...this.state.generalErrors[i]}/>)
}
return <>
<div className={"card-header"} style={{cursor: "pointer"}}
onClick={() => this.toggleCollapse("generalOpened")}>
<h4 className={"card-title"}>
<Icon className={"mr-2"} icon={"cogs"}/>
General Settings
</h4>
<div className={"card-tools"}>
<span className={"btn btn-tool btn-sm"}>
<Icon icon={this.state.generalOpened ? "angle-up" : "angle-down"}/>
</span>
</div>
</div>
<Collapse isOpened={this.state.generalOpened}>
<div className={"card-body"}>
<div className={"row"}>
<div className={"col-12 col-lg-6"}>
{errors}
<div className={"form-group"}>
<label htmlFor={"site_name"}>Site Name</label>
<input type={"text"} className={"form-control"}
value={this.state.settings["site_name"] ?? ""}
placeholder={"Enter a title"} name={"site_name"} id={"site_name"}
onChange={this.onChangeValue.bind(this)}/>
</div>
<div className={"form-group"}>
<label htmlFor={"base_url"}>Base URL</label>
<input type={"text"} className={"form-control"}
value={this.state.settings["base_url"] ?? ""}
placeholder={"Enter a url"} name={"base_url"} id={"base_url"}
onChange={this.onChangeValue.bind(this)}/>
</div>
<div className={"form-group"}>
<label htmlFor={"user_registration_enabled"}>User Registration</label>
<div className={"form-check"}>
<input type={"checkbox"} className={"form-check-input"}
name={"user_registration_enabled"}
id={"user_registration_enabled"}
defaultChecked={(this.state.settings["user_registration_enabled"] ?? "0") === "1"}
onChange={this.onChangeValue.bind(this)}/>
<label className={"form-check-label"}
htmlFor={"user_registration_enabled"}>
Allow anyone to register an account
</label>
</div>
</div>
<div>
<button className={"btn btn-secondary ml-2"} onClick={() => this.onReset("generalErrors", this.generalKeys)}
disabled={this.state.isResetting || this.state.isSaving}>
{this.state.isResetting ?
<span>Resetting&nbsp;<Icon icon={"circle-notch"}/></span> : "Reset"}
</button>
<button className={"btn btn-success ml-2"} onClick={() => this.onSave("generalErrors", this.generalKeys)}
disabled={this.state.isResetting || this.state.isSaving}>
{this.state.isSaving ?
<span>Saving&nbsp;<Icon icon={"circle-notch"}/></span> : "Save"}
</button>
</div>
</div>
</div>
</div>
</Collapse>
</>
}
getEmailCard() {
let errors = [];
for (let i = 0; i < this.state.mailErrors.length; i++) {
errors.push(<Alert key={"error-" + i} onClose={() => this.removeError("mailErrors", i)} {...this.state.mailErrors[i]}/>)
}
return <>
<div className={"card-header"} style={{cursor: "pointer"}}
onClick={() => this.toggleCollapse("mailOpened")}>
<h4 className={"card-title"}>
<Icon className={"mr-2"} icon={"envelope"}/>
Mail Settings
</h4>
<div className={"card-tools"}>
<span className={"btn btn-tool btn-sm"}>
<Icon icon={this.state.mailOpened ? "angle-up" : "angle-down"}/>
</span>
</div>
</div>
<Collapse isOpened={this.state.mailOpened}>
<div className={"card-body"}>
<div className={"row"}>
<div className={"col-12 col-lg-6"}>
{errors}
<div className={"form-group mt-2"}>
<div className={"form-check"}>
<input type={"checkbox"} className={"form-check-input"}
name={"mail_enabled"} id={"mail_enabled"}
checked={(this.state.settings["mail_enabled"] ?? "0") === "1"}
onChange={this.onChangeValue.bind(this)}/>
<label className={"form-check-label"} htmlFor={"mail_enabled"}>
Enable E-Mail service
</label>
</div>
<hr className={"m-3"}/>
<label htmlFor={"mail_username"}>Username</label>
<div className={"input-group"}>
<div className={"input-group-prepend"}>
<span className={"input-group-text"}>
<Icon icon={"hashtag"}/>
</span>
</div>
<input type={"text"} className={"form-control"}
value={this.state.settings["mail_username"] ?? ""}
placeholder={"Enter a username"} name={"mail_username"}
id={"mail_username"} onChange={this.onChangeValue.bind(this)}
disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1"}/>
</div>
<label htmlFor={"mail_password"} className={"mt-2"}>Password</label>
<div className={"input-group"}>
<div className={"input-group-prepend"}>
<span className={"input-group-text"}>
<Icon icon={"key"}/>
</span>
</div>
<input type={"password"} className={"form-control"}
value={this.state.settings["mail_password"] ?? ""}
placeholder={"(unchanged)"} name={"mail_password"}
id={"mail_password"} onChange={this.onChangeValue.bind(this)}
disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1"}/>
</div>
<label htmlFor={"mail_from"} className={"mt-2"}>Sender Email Address</label>
<div className={"input-group"}>
<div className={"input-group-prepend"}>
<span className={"input-group-text"}>@</span>
</div>
<input type={"email"} className={"form-control"}
value={this.state.settings["mail_from"] ?? ""}
placeholder={"Enter a email address"} name={"mail_from"}
id={"mail_from"} onChange={this.onChangeValue.bind(this)}
disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1"}/>
</div>
<div className={"row"}>
<div className={"col-6"}>
<label htmlFor={"mail_host"} className={"mt-2"}>SMTP Host</label>
<div className={"input-group"}>
<div className={"input-group-prepend"}>
<span className={"input-group-text"}>
<Icon icon={"project-diagram"}/>
</span>
</div>
<input type={"text"} className={"form-control"}
value={this.state.settings["mail_host"] ?? ""}
placeholder={"e.g. smtp.example.com"} name={"mail_host"}
id={"mail_host"} onChange={this.onChangeValue.bind(this)}
disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1"}/>
</div>
</div>
<div className={"col-6"}>
<label htmlFor={"mail_port"} className={"mt-2"}>SMTP Port</label>
<div className={"input-group"}>
<div className={"input-group-prepend"}>
<span className={"input-group-text"}>
<Icon icon={"project-diagram"}/>
</span>
</div>
<input type={"number"} className={"form-control"}
value={parseInt(this.state.settings["mail_port"] ?? "25")}
placeholder={"smtp port"} name={"mail_port"}
id={"mail_port"} onChange={this.onChangeValue.bind(this)}
disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1"}/>
</div>
</div>
</div>
</div>
<div>
<button className={"btn btn-secondary ml-2"}
onClick={() => this.onReset("mailErrors", this.mailKeys)}
disabled={this.state.isResetting || this.state.isSaving}>
{this.state.isResetting ?
<span>Resetting&nbsp;<Icon icon={"circle-notch"}/></span> : "Reset"}
</button>
<button className={"btn btn-success ml-2"}
onClick={() => this.onSave("mailErrors", this.mailKeys)}
disabled={this.state.isResetting || this.state.isSaving}>
{this.state.isSaving ?
<span>Saving&nbsp;<Icon icon={"circle-notch"}/></span> : "Save"}
</button>
</div>
<div className={"mt-3"}>
<label htmlFor={"mail_from"} className={"mt-2"}>Send Test E-Mail</label>
<div className={"input-group"}>
<div className={"input-group-prepend"}>
<span className={"input-group-text"}>@</span>
</div>
<input type={"email"} className={"form-control"}
value={this.state.test_email}
placeholder={"Enter a email address"}
onChange={(e) => this.setState({
...this.state,
test_email: e.target.value
})}
disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1"}/>
</div>
<div className={"form-group form-inline mt-3"}>
<button className={"btn btn-info col-2"}
onClick={() => this.onSendTestMail()}
disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1" || this.state.isSending}>
{this.state.isSending ?
<span>Sending&nbsp;<Icon icon={"circle-notch"}/></span> : "Send Mail"}
</button>
<div className={"col-10"}>
{ this.state.unsavedMailSettings ? <span className={"text-red"}>You need to save your mail settings first.</span> : null }
</div>
</div>
</div>
</div>
</div>
</div>
</Collapse>
</>
}
getUncategorisedCard() {
let keys = [];
let tr = [];
for (let key in this.state.settings) {
if (this.state.settings.hasOwnProperty(key)) {
if (!this.generalKeys.includes(key) && !this.mailKeys.includes(key)) {
keys.push(key);
tr.push(<tr key={"tr-" + key}>
<td>{key}</td>
<td>{this.state.settings[key]}</td>
</tr>);
}
}
}
let errors = [];
for (let i = 0; i < this.state.etcErrors.length; i++) {
errors.push(<Alert key={"error-" + i} onClose={() => this.removeError("etcErrors", i)} {...this.state.etcErrors[i]}/>)
}
return <>
<div className={"card-header"} style={{cursor: "pointer"}}
onClick={() => this.toggleCollapse("etcOpened")}>
<h4 className={"card-title"}>
<Icon className={"mr-2"} icon={"cogs"}/>
General Settings
</h4>
<div className={"card-tools"}>
<span className={"btn btn-tool btn-sm"}>
<Icon icon={this.state.etcOpened ? "angle-up" : "angle-down"}/>
</span>
</div>
</div>
<Collapse isOpened={this.state.etcOpened}>
<div className={"card-body"}>
<div className={"row"}>
<div className={"col-12 col-lg-6"}>
{errors}
<table>
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{tr}
</tbody>
</table>
<div>
<button className={"btn btn-secondary ml-2"} onClick={() => this.onReset("etcErrors", keys)}
disabled={this.state.isResetting || this.state.isSaving}>
{this.state.isResetting ?
<span>Resetting&nbsp;<Icon icon={"circle-notch"}/></span> : "Reset"}
</button>
<button className={"btn btn-success ml-2"} onClick={() => this.onSave("etcErrors", keys)}
disabled={this.state.isResetting || this.state.isSaving}>
{this.state.isSaving ?
<span>Saving&nbsp;<Icon icon={"circle-notch"}/></span> : "Save"}
</button>
</div>
</div>
</div>
</div>
</Collapse>
</>
}
render() {
let errors = [];
for (let i = 0; i < this.state.errors.length; i++) {
errors.push(<Alert key={"error-" + i} onClose={() => this.removeError(i)} {...this.state.errors[i]}/>)
errors.push(<Alert key={"error-" + i} onClose={() => this.removeError("errors", i)} {...this.state.errors[i]}/>)
}
return <>
@@ -73,80 +379,13 @@ export default class Settings extends React.Component {
{errors}
<div>
<div className={"card card-primary"}>
<div className={"card-header"} style={{cursor: "pointer"}} onClick={() => this.toggleCollapse("generalOpened")}>
<h4 className={"card-title"}>
<Icon className={"mr-2"} icon={"cogs"} />
General Settings
</h4>
<div className={"card-tools"}>
<span className={"btn btn-tool btn-sm"}>
<Icon icon={ this.state.generalOpened ? "angle-up" : "angle-down" }/>
</span>
</div>
</div>
<Collapse isOpened={this.state.generalOpened}>
<div className={"card-body"}>
<div className={"row"}>
<div className={"col-12 col-lg-6"}>
<div className={"form-group"}>
<label htmlFor={"site_name"}>Site Name</label>
<input type={"text"} className={"form-control"} value={this.state.settings["site_name"] ?? ""}
placeholder={"Enter a title"} name={"site_name"} id={"site_name"} onChange={this.onChangeValue.bind(this)} />
</div>
<div className={"form-group"}>
<label htmlFor={"base_url"}>Base URL</label>
<input type={"text"} className={"form-control"} value={this.state.settings["base_url"] ?? ""}
placeholder={"Enter a url"} name={"base_url"} id={"base_url"} onChange={this.onChangeValue.bind(this)} />
</div>
<div className={"form-group"}>
<label htmlFor={"user_registration_enabled"}>User Registration</label>
<div className={"form-check"}>
<input type={"checkbox"} className={"form-check-input"} name={"user_registration_enabled"} id={"user_registration_enabled"}
defaultChecked={(this.state.settings["user_registration_enabled"] ?? "0") === "1"}
onChange={this.onChangeValue.bind(this)} />
<label className={"form-check-label"} htmlFor={"user_registration_enabled"}>Allow anyone to register an account</label>
</div>
</div>
</div>
</div>
</div>
</Collapse>
{this.getGeneralCard()}
</div>
<div className={"card card-warning"}>
<div className={"card-header"} style={{cursor: "pointer"}} onClick={() => this.toggleCollapse("mailOpened")}>
<h4 className={"card-title"}>
<Icon className={"mr-2"} icon={"envelope"} />
Mail Settings
</h4>
<div className={"card-tools"}>
<span className={"btn btn-tool btn-sm"}>
<Icon icon={ this.state.generalOpened ? "angle-up" : "angle-down" }/>
</span>
</div>
</div>
<Collapse isOpened={this.state.mailOpened}>
<div className={"card-body"}>
</div>
</Collapse>
{this.getEmailCard()}
</div>
<div className={"card card-secondary"}>
<div className={"card-header"} style={{cursor: "pointer"}} onClick={() => this.toggleCollapse("etcOpened")}>
<h4 className={"card-title"}>
<Icon className={"mr-2"} icon={"stream"} />
Uncategorised
</h4>
<div className={"card-tools"}>
<span className={"btn btn-tool btn-sm"}>
<Icon icon={ this.state.generalOpened ? "angle-up" : "angle-down" }/>
</span>
</div>
</div>
<Collapse isOpened={this.state.etcOpened}>
<div className={"card-body"}>
</div>
</Collapse>
{this.getUncategorisedCard()}
</div>
</div>
</div>
@@ -163,6 +402,81 @@ export default class Settings extends React.Component {
value = event.target.checked ? "1" : "0";
}
this.setState({ ...this.state, user: { ...this.state.user, settings: { ...this.state.settings, [name]: value} } });
let changedMailSettings = false;
if (name.startsWith("mail_")) {
changedMailSettings = true;
}
this.setState({...this.state, settings: {...this.state.settings, [name]: value},
unsavedMailSettings: changedMailSettings ? true : this.state.unsavedMailSettings
});
}
onReset(errorKey, keys) {
this.setState({...this.state, isResetting: true});
let values = {};
for (let key of keys) {
values[key] = this.state.settings[key];
}
let mailSettingsSaved = errorKey === "mailErrors";
this.parent.api.getSettings().then((res) => {
if (!res.success) {
let errors = this.state[errorKey].slice();
errors.push({title: "Error fetching settings", message: res.msg});
this.setState({...this.state, [errorKey]: errors, isResetting: false});
} else {
let newSettings = {...this.state.settings};
for (let key of keys) {
newSettings[key] = res.settings[key] ?? "";
}
this.setState({...this.state, settings: newSettings, isResetting: false,
unsavedMailSettings: mailSettingsSaved ? false : this.state.unsavedMailSettings});
}
});
}
onSave(errorKey, keys) {
this.setState({...this.state, isSaving: true});
let values = {};
for (let key of keys) {
if (key === "mail_password" && !this.state.settings[key]) {
continue;
}
values[key] = this.state.settings[key];
}
let mailSettingsSaved = errorKey === "mailErrors";
this.parent.api.saveSettings(values).then((res) => {
if (!res.success) {
let errors = this.state[errorKey].slice();
errors.push({title: "Error fetching settings", message: res.msg});
this.setState({...this.state, [errorKey]: errors, isSaving: false});
} else {
this.setState({...this.state, isSaving: false, unsavedMailSettings: mailSettingsSaved ? false : this.state.unsavedMailSettings });
}
});
}
onSendTestMail() {
this.setState({...this.state, isSending: true});
this.parent.api.sendTestMail(this.state.test_email).then((res) => {
let errors = this.state.mailErrors.slice();
if (!res.success) {
errors.push({title: "Error sending email", message: res.msg});
this.setState({...this.state, mailErrors: errors, isSending: false});
} else {
errors.push({
title: "Success!",
message: "E-Mail was successfully sent, check your inbox.",
type: "success"
});
this.setState({...this.state, mailErrors: errors, isSending: false, test_email: ""});
}
});
}
}