Routing interface
This commit is contained in:
		
							parent
							
								
									78c5934480
								
							
						
					
					
						commit
						8d1c4836e7
					
				| @ -1,9 +1,11 @@ | ||||
| php_flag display_errors on | ||||
| Options -Indexes | ||||
| 
 | ||||
| RedirectMatch 404 /\.idea | ||||
| RedirectMatch 404 /\.git | ||||
| RedirectMatch 404 /src | ||||
| RedirectMatch 404 /test | ||||
| RedirectMatch 404 /core | ||||
| 
 | ||||
| RewriteEngine On | ||||
| RewriteRule ^api(/.*)?$ /index.php?api=$1 [L,QSA] | ||||
|  | ||||
| @ -16,6 +16,8 @@ class Parameter { | ||||
| 
 | ||||
|   // only internal access
 | ||||
|   const TYPE_RAW       = 8; | ||||
| 
 | ||||
|   // only json will work here i guess
 | ||||
|   const TYPE_ARRAY     = 9; | ||||
| 
 | ||||
|   const names = array('Integer', 'Float', 'Boolean', 'String', 'Date', 'Time', 'DateTime', 'E-Mail', 'Raw', 'Array'); | ||||
|  | ||||
| @ -38,7 +38,7 @@ class Fetch extends Request { | ||||
|           "request" => $row["request"], | ||||
|           "action"  => $row["action"], | ||||
|           "target"  => $row["target"], | ||||
|           "extra"   => $row["extra"], | ||||
|           "extra"   => $row["extra"] ?? "", | ||||
|           "active"  => intval($row["active"]), | ||||
|         ); | ||||
|       } | ||||
|  | ||||
							
								
								
									
										99
									
								
								core/Api/Routes/Save.class.php
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										99
									
								
								core/Api/Routes/Save.class.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,99 @@ | ||||
| <?php | ||||
| 
 | ||||
| namespace Api\Routes; | ||||
| 
 | ||||
| use Api\Parameter\Parameter; | ||||
| use \Api\Request; | ||||
| 
 | ||||
| class Save extends Request { | ||||
| 
 | ||||
|   private array $routes; | ||||
| 
 | ||||
|   public function __construct($user, $externalCall = false) { | ||||
|     parent::__construct($user, $externalCall, array( | ||||
|       'routes' => new Parameter('routes',Parameter::TYPE_ARRAY, false) | ||||
|     )); | ||||
| 
 | ||||
|     $this->loginRequired = true; | ||||
|     $this->csrfTokenRequired = true; | ||||
|     $this->requiredGroup = USER_GROUP_ADMIN; | ||||
|   } | ||||
| 
 | ||||
|   public function execute($values = array()) { | ||||
|     if(!parent::execute($values)) { | ||||
|       return false; | ||||
|     } | ||||
| 
 | ||||
|     if (!$this->validateRoutes()) { | ||||
|       return false; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     $sql = $this->user->getSQL(); | ||||
| 
 | ||||
|     // DELETE old rules
 | ||||
|     $this->success = ($sql->truncate("Route")->execute() !== FALSE); | ||||
|     $this->lastError = $sql->getLastError(); | ||||
| 
 | ||||
|     // INSERT new routes
 | ||||
|     if ($this->success) { | ||||
|       $stmt = $sql->insert("Route", array("request", "action", "target", "extra", "active")); | ||||
|       foreach($this->routes as $route) { | ||||
|         $stmt->addRow($route["request"], $route["action"], $route["target"], $route["extra"], $route["active"]); | ||||
|       } | ||||
|       $this->success = ($stmt->execute() !== FALSE); | ||||
|       $this->lastError = $sql->getLastError(); | ||||
|     } | ||||
| 
 | ||||
|     return $this->success; | ||||
|   } | ||||
| 
 | ||||
|   private function validateRoutes() { | ||||
| 
 | ||||
|     $this->routes = array(); | ||||
|     $keys = array( | ||||
|       "request" => Parameter::TYPE_STRING, | ||||
|       "action" => Parameter::TYPE_STRING, | ||||
|       "target" => Parameter::TYPE_STRING, | ||||
|       "extra"  => Parameter::TYPE_STRING, | ||||
|       "active" => Parameter::TYPE_BOOLEAN | ||||
|     ); | ||||
| 
 | ||||
|     $actions = array( | ||||
|       "redirect_temporary", "redirect_permanently", "static", "dynamic" | ||||
|     ); | ||||
| 
 | ||||
|     foreach($this->getParam("routes") as $index => $route) { | ||||
|       foreach($keys as $key => $expectedType) { | ||||
|         if (!array_key_exists($key, $route)) { | ||||
|           return $this->createError("Route $index missing key: $key"); | ||||
|         } | ||||
| 
 | ||||
|         $value = $route[$key]; | ||||
|         $type = Parameter::parseType($value); | ||||
|         if ($type !== $expectedType && ($key !== "active" || !is_null($value))) { | ||||
|           $expectedTypeName = Parameter::names[$expectedType]; | ||||
|           $gotTypeName = Parameter::names[$type]; | ||||
|           return $this->createError("Route $index has invalid value for key: $key, expected: $expectedTypeName, got: $gotTypeName"); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       $action = $route["action"]; | ||||
|       if (!in_array($action, $actions)) { | ||||
|         return $this->createError("Invalid action: $action"); | ||||
|       } | ||||
| 
 | ||||
|       if(empty($route["request"])) { | ||||
|         return $this->createError("Request cannot be empty."); | ||||
|       } | ||||
| 
 | ||||
|       if(empty($route["target"])) { | ||||
|         return $this->createError("Target cannot be empty."); | ||||
|       } | ||||
| 
 | ||||
|       $this->routes[] = $route; | ||||
|     } | ||||
| 
 | ||||
|     return true; | ||||
|   } | ||||
| } | ||||
| @ -129,8 +129,7 @@ class CreateDatabase { | ||||
|       ->primaryKey("uid"); | ||||
| 
 | ||||
|     $queries[] = $sql->insert("Route", array("request", "action", "target")) | ||||
|       ->addRow("/admin(/.*)?", "dynamic", "\\Core\\Documents\\AdminDashboard") | ||||
|       ->addRow("/api/(.*)", "dynamic", "\\Core\\Api\\$1"); | ||||
|       ->addRow("/admin(/.*)?", "dynamic", "\\Documents\\AdminDashboard"); | ||||
| 
 | ||||
|     return $queries; | ||||
|   } | ||||
|  | ||||
							
								
								
									
										4
									
								
								js/admin.min.js
									
									
									
									
										vendored
									
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										4
									
								
								js/admin.min.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @ -61,4 +61,8 @@ export default class API { | ||||
|     async getRoutes() { | ||||
|         return this.apiCall("routes/fetch"); | ||||
|     } | ||||
| 
 | ||||
|     async saveRoutes(routes) { | ||||
|         return this.apiCall("routes/save", { "routes": routes }); | ||||
|     } | ||||
| }; | ||||
| @ -15,22 +15,31 @@ export default class PageOverview extends React.Component { | ||||
| 
 | ||||
|         this.state = { | ||||
|             routes: [], | ||||
|             errors: [] | ||||
|             errors: [], | ||||
|             isResetting: false, | ||||
|             isSaving: false | ||||
|         }; | ||||
| 
 | ||||
|         this.options = { | ||||
|         this.optionMap = { | ||||
|             "redirect_temporary": "Redirect Temporary", | ||||
|             "redirect_permanently": "Redirect Permanently", | ||||
|             "static": "Serve Static", | ||||
|             "dynamic": "Load Dynamic", | ||||
|         }; | ||||
| 
 | ||||
|         this.options = []; | ||||
|         for (let key in this.optionMap) { | ||||
|             this.options.push(this.buildOption(key)); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     buildOption(key) { | ||||
|         if (typeof key === 'object' && key.hasOwnProperty("key") && key.hasOwnProperty("label")) { | ||||
|             return key; | ||||
|         } else if (typeof key === 'string') { | ||||
|             return { value: key, label: this.optionMap[key] }; | ||||
|         } else { | ||||
|             return { value: key, label: this.options[key] }; | ||||
|             return this.options[key]; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @ -43,15 +52,7 @@ export default class PageOverview extends React.Component { | ||||
|     } | ||||
| 
 | ||||
|     componentDidMount() { | ||||
|         this.parent.api.getRoutes().then((res) => { | ||||
|             if (res.success) { | ||||
|                 this.setState({...this.state, routes: res.routes}); | ||||
|             } else { | ||||
|                 let errors = this.state.errors.slice(); | ||||
|                 errors.push({ title: "Error fetching routes", message: res.msg }); | ||||
|                 this.setState({...this.state, errors: errors}); | ||||
|             } | ||||
|         }); | ||||
|         this.fetchRoutes() | ||||
|     } | ||||
| 
 | ||||
|     render() { | ||||
| @ -63,27 +64,41 @@ export default class PageOverview extends React.Component { | ||||
|             errors.push(<Alert key={"error-" + i} onClose={() => this.removeError(i)} {...this.state.errors[i]}/>) | ||||
|         } | ||||
| 
 | ||||
|         let options = []; | ||||
|         for (let key in Object.keys(this.options)) { | ||||
|             options.push(this.buildOption(key)); | ||||
|         } | ||||
|         const inputStyle = { fontFamily: "Courier", paddingTop: "14px" }; | ||||
| 
 | ||||
|         for (let i = 0; i <  this.state.routes.length; i++) { | ||||
|             let route = this.state.routes[i]; | ||||
|             rows.push( | ||||
|                 <tr key={"route-" + i}> | ||||
|                     <td className={"align-middle"}>{route.request}</td> | ||||
|                     <td className={"text-center"}> | ||||
|                         <Select options={options} value={this.buildOption(route.action)} onChange={(selectedOption) => this.changeAction(i, selectedOption)} /> | ||||
|                     <td className={"align-middle"}> | ||||
|                         <input type={"text"} maxLength={128} style={inputStyle} className={"form-control"} | ||||
|                                value={route.request} onChange={(e) => this.changeRequest(i, e)} /> | ||||
|                     </td> | ||||
|                     <td className={"text-center"}> | ||||
|                         <Select options={this.options} value={this.buildOption(route.action)} onChange={(selectedOption) => this.changeAction(i, selectedOption)} /> | ||||
|                     </td> | ||||
|                     <td className={"align-middle"}> | ||||
|                         <input type={"text"} maxLength={128} style={inputStyle} className={"form-control"} | ||||
|                                value={route.target} onChange={(e) => this.changeTarget(i, e)} /> | ||||
|                     </td> | ||||
|                     <td className={"align-middle"}> | ||||
|                         <input type={"text"} maxLength={64} style={inputStyle} className={"form-control"} | ||||
|                                value={route.extra} onChange={(e) => this.changeExtra(i, e)} /> | ||||
|                     </td> | ||||
|                     <td className={"align-middle"}>{route.target}</td> | ||||
|                     <td className={"align-middle"}>{route.extra}</td> | ||||
|                     <td className={"text-center"}> | ||||
|                         <input | ||||
|                             type={"checkbox"} | ||||
|                             checked={route.active === 1} | ||||
|                             onChange={(e) => this.changeActive(i, e)} /> | ||||
|                     </td> | ||||
|                     <td> | ||||
|                         <ReactTooltip id={"delete-" + i} /> | ||||
|                         <Icon icon={"trash"} style={{color: "red", cursor: "pointer"}} | ||||
|                               data-tip={"Click to delete this route"} | ||||
|                               data-type={"warning"} data-place={"right"} | ||||
|                               data-for={"delete-" + i} data-effect={"solid"} | ||||
|                               onClick={() => this.removeRoute(i)}/> | ||||
|                     </td> | ||||
|                 </tr> | ||||
|             ); | ||||
|         } | ||||
| @ -108,76 +123,152 @@ export default class PageOverview extends React.Component { | ||||
|                 {errors} | ||||
|                 <div className={"content-fluid"}> | ||||
|                     <div className={"row"}> | ||||
|                         <div className={"col-lg-8 col-12"}> | ||||
|                         <div className={"col-lg-10 col-12"}> | ||||
|                             <table className={"table"}> | ||||
|                                 <thead className={"thead-dark"}> | ||||
|                                     <tr> | ||||
|                                         <td> | ||||
|                                         <th> | ||||
|                                             Request  | ||||
|                                             <Icon icon={"question-circle"} style={{"color": "#0069d9"}} | ||||
|                                             <Icon icon={"question-circle"} style={{"color": "#17a2b8"}} | ||||
|                                                   data-tip={"The request, the user is making. Can also be interpreted as a regular expression."} | ||||
|                                                   data-type={"info"} data-place={"bottom"} data-effect={"solid"}/> | ||||
|                                         </td> | ||||
|                                         <td style={{minWidth: "250px"}}> | ||||
|                                                   data-type={"info"} data-place={"bottom"}/> | ||||
|                                         </th> | ||||
|                                         <th style={{minWidth: "250px"}}> | ||||
|                                             Action  | ||||
|                                             <Icon icon={"question-circle"} style={{"color": "#0069d9"}} | ||||
|                                             <Icon icon={"question-circle"} style={{"color": "#17a2b8"}} | ||||
|                                                   data-tip={"The action to be taken"} | ||||
|                                                   data-type={"info"} data-place={"bottom"} data-effect={"solid"}/> | ||||
|                                         </td> | ||||
|                                         <td> | ||||
|                                                   data-type={"info"} data-place={"bottom"}/> | ||||
|                                         </th> | ||||
|                                         <th> | ||||
|                                             Target  | ||||
|                                             <Icon icon={"question-circle"} style={{"color": "#0069d9"}} | ||||
|                                             <Icon icon={"question-circle"} style={{"color": "#17a2b8"}} | ||||
|                                                   data-tip={"Any URL if action is redirect or static. Path to a class inheriting from Document, " + | ||||
|                                                   "if dynamic is chosen"} | ||||
|                                                   data-type={"info"} data-place={"bottom"} data-effect={"solid"}/> | ||||
|                                         </td> | ||||
|                                         <td> | ||||
|                                                   data-type={"info"} data-place={"bottom"}/> | ||||
|                                         </th> | ||||
|                                         <th> | ||||
|                                             Extra  | ||||
|                                             <Icon icon={"question-circle"} style={{"color": "#0069d9"}} | ||||
|                                             <Icon icon={"question-circle"} style={{"color": "#17a2b8"}} | ||||
|                                                   data-tip={"If action is dynamic, a view name can be entered here, otherwise leave empty."} | ||||
|                                                   data-type={"info"} data-place={"bottom"} data-effect={"solid"}/> | ||||
|                                         </td> | ||||
|                                         <td className={"text-center"}> | ||||
|                                                   data-type={"info"} data-place={"bottom"}/> | ||||
|                                         </th> | ||||
|                                         <th className={"text-center"}> | ||||
|                                             Active  | ||||
|                                             <Icon icon={"question-circle"} style={{"color": "#0069d9"}} | ||||
|                                             <Icon icon={"question-circle"} style={{"color": "#17a2b8"}} | ||||
|                                                   data-tip={"True, if the route is currently active."} | ||||
|                                                   data-type={"info"} data-place={"bottom"} data-effect={"solid"}/> | ||||
|                                         </td> | ||||
|                                                   data-type={"info"} data-place={"bottom"}/> | ||||
|                                         </th> | ||||
|                                         <th/> | ||||
|                                     </tr> | ||||
|                                 </thead> | ||||
|                                 <tbody> | ||||
|                                     {rows} | ||||
|                                 </tbody> | ||||
|                             </table> | ||||
|                             <div> | ||||
|                                 <button className={"btn btn-info"} onClick={() => this.onAddRoute()} disabled={this.state.isResetting || this.state.isSaving}> | ||||
|                                     <Icon icon={"plus"}/> Add new Route | ||||
|                                 </button> | ||||
|                                 <button className={"btn btn-secondary ml-2"} onClick={() => this.onResetRoutes()} 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.onSaveRoutes()} disabled={this.state.isResetting || this.state.isSaving}> | ||||
|                                     { this.state.isSaving ? <span>Saving <Icon icon={"circle-notch"}/></span> : "Save" } | ||||
|                                 </button> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <ReactTooltip/> | ||||
|             <ReactTooltip data-effect={"solid"}/> | ||||
|         </> | ||||
|     } | ||||
| 
 | ||||
|     changeAction(index, selectedOption) { | ||||
|     onResetRoutes() { | ||||
|         this.setState({ ...this.state, isResetting: true }); | ||||
|         this.fetchRoutes(); | ||||
|     } | ||||
| 
 | ||||
|     onSaveRoutes() { | ||||
|         this.setState({ ...this.state, isSaving: true }); | ||||
| 
 | ||||
|         let routes = []; | ||||
|         for (let i = 0; i < this.state.routes.length; i++) { | ||||
|             let route = this.state.routes[i]; | ||||
|             routes.push({ | ||||
|                 request: route.request, | ||||
|                 action: typeof route.action === 'object' ? route.action.value : route.action, | ||||
|                 target: route.target, | ||||
|                 extra: route.extra ?? "", | ||||
|                 active: route.active === 1 | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         this.parent.api.saveRoutes(routes).then((res) => { | ||||
|             if (res.success) { | ||||
|                 this.setState({...this.state, isSaving: false}); | ||||
|             } else { | ||||
|                 let errors = this.state.errors.slice(); | ||||
|                 errors.push({ title: "Error saving routes", message: res.msg }); | ||||
|                 this.setState({...this.state, errors: errors, isSaving: false}); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     changeRoute(index, key, value) { | ||||
|         if (index < 0 || index >= this.state.routes.length) | ||||
|             return; | ||||
| 
 | ||||
|         let routes = this.state.routes.slice(); | ||||
|         routes[index].action = selectedOption; | ||||
|         this.setState({ | ||||
|             ...this.state, | ||||
|             routes: routes | ||||
|         }); | ||||
|         routes[index][key] = value; | ||||
|         this.setState({ ...this.state, routes: routes }); | ||||
|     } | ||||
| 
 | ||||
|     removeRoute(index) { | ||||
|         if (index < 0 || index >= this.state.routes.length) | ||||
|             return; | ||||
|         let routes = this.state.routes.slice(); | ||||
|         routes.splice(index, 1); | ||||
|         this.setState({ ...this.state, routes: routes }); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     onAddRoute() { | ||||
|         let routes = this.state.routes.slice(); | ||||
|         routes.push({ request: "", action: "dynamic", target: "", extra: "", active: 1 }); | ||||
|         this.setState({ ...this.state, routes: routes }); | ||||
|     } | ||||
| 
 | ||||
|     changeAction(index, selectedOption) { | ||||
|         this.changeRoute(index, "action", selectedOption); | ||||
|     } | ||||
| 
 | ||||
|     changeActive(index, e) { | ||||
|         if (index < 0 || index >= this.state.routes.length) | ||||
|             return; | ||||
|         this.changeRoute(index, "active", e.target.checked ? 1 : 0); | ||||
|     } | ||||
| 
 | ||||
|         let routes = this.state.routes.slice(); | ||||
|         routes[index].active = e.target.checked ? 1 : 0; | ||||
|         this.setState({ | ||||
|             ...this.state, | ||||
|             routes: routes | ||||
|     changeRequest(index, e) { | ||||
|         this.changeRoute(index, "request", e.target.value); | ||||
|     } | ||||
| 
 | ||||
|     changeTarget(index, e) { | ||||
|         this.changeRoute(index, "target", e.target.value); | ||||
|     } | ||||
| 
 | ||||
|     changeExtra(index, e) { | ||||
|         this.changeRoute(index, "extra", e.target.value); | ||||
|     } | ||||
| 
 | ||||
|     fetchRoutes() { | ||||
|         this.parent.api.getRoutes().then((res) => { | ||||
|             if (res.success) { | ||||
|                 this.setState({...this.state, routes: res.routes, isResetting: false}); | ||||
|                 ReactTooltip.rebuild(); | ||||
|             } else { | ||||
|                 let errors = this.state.errors.slice(); | ||||
|                 errors.push({ title: "Error fetching routes", message: res.msg }); | ||||
|                 this.setState({...this.state, errors: errors, isResetting: false}); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| } | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user