Settings + Test Mail

This commit is contained in:
Roman Hergenreder 2020-06-26 14:58:17 +02:00
parent 6eb9bf333f
commit 09475be545
11 changed files with 532 additions and 108 deletions

@ -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";

@ -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;
}
}

@ -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();

@ -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 {

@ -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
));
}
}

@ -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;
}
}

@ -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;

4
js/admin.min.js vendored

File diff suppressed because one or more lines are too long

@ -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 });
}
};

@ -11,46 +11,352 @@ 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() {
this.parent.api.getSettings().then((res) => {
if (res.success) {
this.setState({...this.state, settings: res.settings });
this.setState({...this.state, settings: res.settings});
} else {
let errors = this.state.errors.slice();
errors.push({ title: "Error fetching settings", message: res.msg });
errors.push({title: "Error fetching settings", message: res.msg});
this.setState({...this.state, errors: errors});
}
});
}
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});
}
}
toggleCollapse(key) {
this.setState({ ...this.state, [key]: !this.state[key] });
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: ""});
}
});
}
}