Mail Templates + Frontend
This commit is contained in:
		
							parent
							
								
									f5a8f2c777
								
							
						
					
					
						commit
						33dd990b71
					
				| @ -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
									
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										14
									
								
								js/admin.min.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										54
									
								
								src/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										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 <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 <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: { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user