Mail Templates + Frontend

This commit is contained in:
Roman Hergenreder 2020-06-26 18:24:23 +02:00
parent f5a8f2c777
commit 33dd990b71
7 changed files with 190 additions and 133 deletions

@ -98,6 +98,18 @@ namespace Api {
return ($this->success && !empty($res) ? $res : array());
}
protected function getMessageTemplate($key) {
$req = new \Api\Settings\Get($this->user);
$this->success = $req->execute(array("key" => $key));
$this->lastError = $req->getLastError();
if ($this->success) {
return $req->getResult()["settings"][$key] ?? "{{link}}";
}
return $this->success;
}
}
}
@ -386,19 +398,39 @@ namespace Api\User {
//send validation mail
if ($this->success) {
$request = new SendMail($this->user);
$link = "http://localhost/acceptInvitation?token=$token";
$this->success = $request->execute(array(
"from" => "webmaster@romanh.de",
$settings = $this->user->getConfiguration()->getSettings();
$baseUrl = htmlspecialchars($settings->getBaseUrl());
$siteName = htmlspecialchars($settings->getSiteName());
$body = $this->getMessageTemplate("message_accept_invite");
if ($this->success) {
$replacements = array(
"link" => "$baseUrl/acceptInvite?token=$token",
"site_name" => $siteName,
"base_url" => $baseUrl,
"username" => htmlspecialchars($username)
);
foreach($replacements as $key => $value) {
$body = str_replace("{{{$key}}}", $value, $body);
}
$request = new SendMail($this->user);
$this->success = $request->execute(array(
"to" => $email,
"subject" => "Account Invitation for web-base@localhost",
"body" =>
"Hello,<br>
you were invited to create an account on web-base@localhost. Click on the following link to confirm the registration, it is 48h valid from now.
If the invitation was not intended, you can simply ignore this email.<br><br><a href=\"$link\">$link</a>"
)
);
$this->lastError = $request->getLastError();
"subject" => "[$siteName] Account Invitation",
"body" => $body
));
$this->lastError = $request->getLastError();
}
if (!$this->success) {
$this->lastError = "The invitation was created but the confirmation email could not be sent. " .
"Please contact the server administration. Reason: " . $this->lastError;
}
}
return $this->success;
}
@ -573,22 +605,36 @@ If the invitation was not intended, you can simply ignore this email.<br><br><a
return false;
}
$request = new SendMail($this->user);
$link = "http://localhost/confirmEmail?token=$this->token";
$this->success = $request->execute(array(
"from" => "webmaster@romanh.de",
"to" => $email,
"subject" => "E-Mail Confirmation for web-base@localhost",
"body" =>
"Hello,<br>
you recently registered an account on web-base@localhost. Click on the following link to confirm the registration, it is 48h valid from now.
If the registration was not intended, you can simply ignore this email.<br><br><a href=\"$link\">$link</a>"
)
);
$settings = $this->user->getConfiguration()->getSettings();
$baseUrl = htmlspecialchars($settings->getBaseUrl());
$siteName = htmlspecialchars($settings->getSiteName());
$body = $this->getMessageTemplate("message_confirm_email");
if ($this->success) {
$replacements = array(
"link" => "$baseUrl/confirmEmail?token=$this->token",
"site_name" => $siteName,
"base_url" => $baseUrl,
"username" => htmlspecialchars($username)
);
foreach($replacements as $key => $value) {
$body = str_replace("{{{$key}}}", $value, $body);
}
$request = new SendMail($this->user);
$this->success = $request->execute(array(
"to" => $email,
"subject" => "[$siteName] E-Mail Confirmation",
"body" => $body
));
$this->lastError = $request->getLastError();
}
if (!$this->success) {
$this->lastError = "Your account was registered but the confirmation email could not be sent. " .
"Please contact the server administration. Reason: " . $request->getLastError();
"Please contact the server administration. Reason: " . $this->lastError;
}
return $this->success;

@ -165,37 +165,31 @@ class CreateDatabase {
}
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
));
return "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}}\">{{link}}</a><br><br> " .
"Best Regards<br> " .
"{{site_name}} Administration";
}
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
));
return "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";
}
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
));
return "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";
}
}

@ -68,4 +68,12 @@ class Settings {
->addRow("installation_completed", $this->installationComplete ? "1" : "0", true)
->addRow("jwt_secret", $this->jwtSecret, true);
}
public function getSiteName() {
return $this->siteName;
}
public function getBaseUrl() {
return $this->baseUrl;
}
}

14
js/admin.min.js vendored

File diff suppressed because one or more lines are too long

54
src/package-lock.json generated

@ -11830,6 +11830,55 @@
"walker": "~1.0.5"
}
},
"sanitize-html": {
"version": "1.27.0",
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-1.27.0.tgz",
"integrity": "sha512-U1btucGeYVpg0GoK43jPpe/bDCV4cBOGuxzv5NBd0bOjyZdMKY0n98S/vNlO1wVwre0VCj8H3hbzE7gD2+RjKA==",
"requires": {
"chalk": "^2.4.1",
"htmlparser2": "^4.1.0",
"lodash": "^4.17.15",
"postcss": "^7.0.27",
"srcset": "^2.0.1",
"xtend": "^4.0.1"
},
"dependencies": {
"domelementtype": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz",
"integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ=="
},
"domhandler": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.0.0.tgz",
"integrity": "sha512-eKLdI5v9m67kbXQbJSNn1zjh0SDzvzWVWtX+qEI3eMjZw8daH9k8rlj1FZY9memPwjiskQFbe7vHVVJIAqoEhw==",
"requires": {
"domelementtype": "^2.0.1"
}
},
"domutils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.1.0.tgz",
"integrity": "sha512-CD9M0Dm1iaHfQ1R/TI+z3/JWp/pgub0j4jIQKH89ARR4ATAV2nbaOQS5XxU9maJP5jHaPdDDQSEHuE2UmpUTKg==",
"requires": {
"dom-serializer": "^0.2.1",
"domelementtype": "^2.0.1",
"domhandler": "^3.0.0"
}
},
"htmlparser2": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-4.1.0.tgz",
"integrity": "sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q==",
"requires": {
"domelementtype": "^2.0.1",
"domhandler": "^3.0.0",
"domutils": "^2.0.0",
"entities": "^2.0.0"
}
}
}
},
"sanitize.css": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-10.0.0.tgz",
@ -12450,6 +12499,11 @@
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
},
"srcset": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/srcset/-/srcset-2.0.1.tgz",
"integrity": "sha512-00kZI87TdRKwt+P8jj8UZxbfp7mK2ufxcIMWvhAOZNJTRROimpHeruWrGvCZneiuVDLqdyHefVp748ECTnyUBQ=="
},
"sshpk": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",

@ -17,7 +17,8 @@
"react-router-dom": "^5.2.0",
"react-scripts": "^3.4.1",
"react-select": "^3.1.0",
"react-tooltip": "^4.2.7"
"react-tooltip": "^4.2.7",
"sanitize-html": "^1.27.0"
},
"scripts": {
"build": "webpack --mode development && mv dist/main.js ../js/admin.min.js"

@ -3,11 +3,13 @@ import {Link} from "react-router-dom";
import Alert from "../elements/alert";
import {Collapse} from "react-collapse/lib/Collapse";
import Icon from "../elements/icon";
import { EditorState, ContentState, convertFromHTML, convertToRaw } from 'draft-js'
import { EditorState, ContentState, convertToRaw } from 'draft-js'
import { Editor } from 'react-draft-wysiwyg'
import draftToHtml from 'draftjs-to-html';
import htmlToDraft from 'html-to-draftjs';
import sanitizeHtml from 'sanitize-html'
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css';
import ReactTooltip from "react-tooltip";
export default class Settings extends React.Component {
@ -302,11 +304,16 @@ export default class Settings extends React.Component {
<div className={"form-group"} key={"group-" + key}>
<label htmlFor={key}>
{ title }
<ReactTooltip id={"tooltip-" + key} />
<Icon icon={"times"} className={"ml-2 text-danger"} style={{cursor: "pointer"}}
onClick={() => this.closeEditor(false)}
onClick={() => this.closeEditor(false)} data-type={"error"}
data-tip={"Discard Changes"} data-place={"top"} data-effect={"solid"}
data-for={"tooltip-" + key}
/>
<Icon icon={"check"} className={"ml-1 text-success"} style={{cursor: "pointer"}}
onClick={() => this.closeEditor(true)}
<Icon icon={"check"} className={"ml-2 text-success"} style={{cursor: "pointer"}}
onClick={() => this.closeEditor(true)} data-type={"success"}
data-tip={"Save Changes"} data-place={"top"} data-effect={"solid"}
data-for={"tooltip-" + key}
/>
</label>
{ editor }
@ -315,15 +322,16 @@ export default class Settings extends React.Component {
} else {
formGroups.push(
<div className={"form-group"} key={"group-" + key}>
<ReactTooltip id={"tooltip-" + key} />
<label htmlFor={key}>
{ title }
<Icon icon={"pencil-alt"} className={"ml-2"} style={{cursor: "pointer"}}
onClick={() => this.openEditor(key)}
onClick={() => this.openEditor(key)} data-type={"info"}
data-tip={"Edit Template"} data-place={"top"} data-effect={"solid"}
data-for={"tooltip-" + key}
/>
</label>
<textarea rows={6} className={"form-control"} disabled={true}
value={this.state.settings[key] ?? ""} id={key} name={key}
/>
<div className={"p-2 text-black"} style={{backgroundColor: "#d2d6de"}} dangerouslySetInnerHTML={{ __html: sanitizeHtml(this.state.settings[key] ?? "") }} />
</div>
);
}
@ -332,77 +340,10 @@ export default class Settings extends React.Component {
return formGroups;
}
/*
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>
</>
getUncategorizedForm() {
return <b>Coming soon</b>
}
*/
render() {
let errors = [];
@ -415,7 +356,7 @@ export default class Settings extends React.Component {
"general": {color: "primary", icon: "cogs", title: "General Settings", content: this.createGeneralForm()},
"mail": {color: "warning", icon: "envelope", title: "Mail Settings", content: this.createMailForm()},
"messages": {color: "info", icon: "copy", title: "Message Templates", content: this.getMessagesForm()},
"uncategorised": {color: "secondary", icon: "stream", title: "Uncategorised", content: <></>},
"uncategorised": {color: "secondary", icon: "stream", title: "Uncategorised", content: this.getUncategorizedForm()},
};
let cards = [];
@ -446,10 +387,11 @@ export default class Settings extends React.Component {
{cards}
</div>
</div>
<ReactTooltip />
</>
}
onEditorStateChange(editorState, key) {
onEditorStateChange(editorState) {
this.setState({
...this.state,
messages: {