Migrate to actix-web #8
					 12 changed files with 933 additions and 2451 deletions
				
			
		
							
								
								
									
										4
									
								
								.dir-locals.el
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.dir-locals.el
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | ||||||
|  | ((nil . ((indent-tabs-mode . nil) | ||||||
|  |          (tab-width . 4) | ||||||
|  |          (fill-column . 80) | ||||||
|  |          (projectile-project-run-cmd . "cargo run -- --dev-mode --template-directory ~/Documents/Projects/tlaternet-templates/result")))) | ||||||
							
								
								
									
										2738
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										2738
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										18
									
								
								Cargo.toml
									
										
									
									
									
								
							
							
						
						
									
										18
									
								
								Cargo.toml
									
										
									
									
									
								
							|  | @ -1,13 +1,15 @@ | ||||||
| [package] | [package] | ||||||
| name = "tlaternet-webserver" | name = "tlaternet-webserver" | ||||||
| version = "0.1.0" | version = "0.1.0" | ||||||
| authors = ["Tristan Daniël Maat <tm@tlater.net>"] | edition = "2021" | ||||||
| edition = "2018" |  | ||||||
| 
 | 
 | ||||||
| [dependencies] | [dependencies] | ||||||
| check-if-email-exists = "0.8.5" | actix-files = "0.6.2" | ||||||
| lettre = "0.9.3" | actix-web = { version = "4.2.1", features = ["macros"] } | ||||||
| lettre_email = "0.9.3" | clap = { version = "3.2.17", features = ["derive"] } | ||||||
| rocket = "0.4.4" | derive_more = "0.99.17" | ||||||
| rocket_contrib = { version = "0.4.4", features = [ "handlebars_templates", "serve" ] } | env_logger = "0.9.0" | ||||||
| serde = { version = "1.0.111", features = [ "derive" ] } | handlebars = { version = "4.3.3", features = ["dir_source"] } | ||||||
|  | log = "0.4.17" | ||||||
|  | serde = {version = "1.0.144", features = ["derive"]} | ||||||
|  | serde_json = "1.0.83" | ||||||
|  |  | ||||||
							
								
								
									
										125
									
								
								flake.lock
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										125
									
								
								flake.lock
									
										
									
										generated
									
									
									
								
							|  | @ -15,21 +15,6 @@ | ||||||
|         "type": "github" |         "type": "github" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "flake-utils_2": { |  | ||||||
|       "locked": { |  | ||||||
|         "lastModified": 1631561581, |  | ||||||
|         "narHash": "sha256-3VQMV5zvxaVLvqqUrNz3iJelLw30mIVSfZmAaauM3dA=", |  | ||||||
|         "owner": "numtide", |  | ||||||
|         "repo": "flake-utils", |  | ||||||
|         "rev": "7e5bf3925f6fbdfaf50a2a7ca0be2879c4261d19", |  | ||||||
|         "type": "github" |  | ||||||
|       }, |  | ||||||
|       "original": { |  | ||||||
|         "owner": "numtide", |  | ||||||
|         "repo": "flake-utils", |  | ||||||
|         "type": "github" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "naersk": { |     "naersk": { | ||||||
|       "inputs": { |       "inputs": { | ||||||
|         "nixpkgs": [ |         "nixpkgs": [ | ||||||
|  | @ -37,11 +22,11 @@ | ||||||
|         ] |         ] | ||||||
|       }, |       }, | ||||||
|       "locked": { |       "locked": { | ||||||
|         "lastModified": 1659610603, |         "lastModified": 1662220400, | ||||||
|         "narHash": "sha256-LYgASYSPYo7O71WfeUOaEUzYfzuXm8c8eavJcel+pfI=", |         "narHash": "sha256-9o2OGQqu4xyLZP9K6kNe1pTHnyPz0Wr3raGYnr9AIgY=", | ||||||
|         "owner": "nmattia", |         "owner": "nmattia", | ||||||
|         "repo": "naersk", |         "repo": "naersk", | ||||||
|         "rev": "c6a45e4277fa58abd524681466d3450f896dc094", |         "rev": "6944160c19cb591eb85bbf9b2f2768a935623ed3", | ||||||
|         "type": "github" |         "type": "github" | ||||||
|       }, |       }, | ||||||
|       "original": { |       "original": { | ||||||
|  | @ -50,28 +35,38 @@ | ||||||
|         "type": "github" |         "type": "github" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "naersk_2": { |     "nix-filter": { | ||||||
|       "inputs": { |  | ||||||
|         "nixpkgs": [ |  | ||||||
|           "tlaternet-templates", |  | ||||||
|           "nixpkgs" |  | ||||||
|         ] |  | ||||||
|       }, |  | ||||||
|       "locked": { |       "locked": { | ||||||
|         "lastModified": 1632266297, |         "lastModified": 1661201956, | ||||||
|         "narHash": "sha256-J1yeJk6Gud9ef2pEf6aKQemrfg1pVngYDSh+SAY94xk=", |         "narHash": "sha256-RizGJH/buaw9A2+fiBf9WnXYw4LZABB5kMAZIEE5/T8=", | ||||||
|         "owner": "nmattia", |         "owner": "numtide", | ||||||
|         "repo": "naersk", |         "repo": "nix-filter", | ||||||
|         "rev": "ee7edec50b49ab6d69b06d62f1de554efccb1ccd", |         "rev": "3b821578685d661a10b563cba30b1861eec05748", | ||||||
|         "type": "github" |         "type": "github" | ||||||
|       }, |       }, | ||||||
|       "original": { |       "original": { | ||||||
|         "owner": "nmattia", |         "owner": "numtide", | ||||||
|         "repo": "naersk", |         "repo": "nix-filter", | ||||||
|         "type": "github" |         "type": "github" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "nixpkgs": { |     "nixpkgs": { | ||||||
|  |       "locked": { | ||||||
|  |         "lastModified": 1663244735, | ||||||
|  |         "narHash": "sha256-+EukKkeAx6ithOLM1u5x4D12ZFuoi6vpPYjhNDmLz1o=", | ||||||
|  |         "owner": "nixos", | ||||||
|  |         "repo": "nixpkgs", | ||||||
|  |         "rev": "178fea1414ae708a5704490f4c49ec3320be9815", | ||||||
|  |         "type": "github" | ||||||
|  |       }, | ||||||
|  |       "original": { | ||||||
|  |         "owner": "nixos", | ||||||
|  |         "ref": "nixos-22.05", | ||||||
|  |         "repo": "nixpkgs", | ||||||
|  |         "type": "github" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "nixpkgs_2": { | ||||||
|       "locked": { |       "locked": { | ||||||
|         "lastModified": 1660318005, |         "lastModified": 1660318005, | ||||||
|         "narHash": "sha256-g9WCa9lVUmOV6dYRbEPjv/TLOR5hamjeCcKExVGS3OQ=", |         "narHash": "sha256-g9WCa9lVUmOV6dYRbEPjv/TLOR5hamjeCcKExVGS3OQ=", | ||||||
|  | @ -87,19 +82,19 @@ | ||||||
|         "type": "github" |         "type": "github" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "nixpkgs_2": { |     "npmlock2nix": { | ||||||
|  |       "flake": false, | ||||||
|       "locked": { |       "locked": { | ||||||
|         "lastModified": 1632408697, |         "lastModified": 1654775747, | ||||||
|         "narHash": "sha256-JqTfu361AwFmV0WszXLAjfukqGxBbHRopRgdp9A2w8s=", |         "narHash": "sha256-9pXHDpIjmsK5390wmpGHu9aA4QOPpegPBvThHeBlef4=", | ||||||
|         "owner": "nixos", |         "owner": "nix-community", | ||||||
|         "repo": "nixpkgs", |         "repo": "npmlock2nix", | ||||||
|         "rev": "a13824fe5e38187fbc75cd598b5c06bdcc13501f", |         "rev": "5c4f247688fc91d665df65f71c81e0726621aaa8", | ||||||
|         "type": "github" |         "type": "github" | ||||||
|       }, |       }, | ||||||
|       "original": { |       "original": { | ||||||
|         "owner": "nixos", |         "owner": "nix-community", | ||||||
|         "ref": "nixos-20.09", |         "repo": "npmlock2nix", | ||||||
|         "repo": "nixpkgs", |  | ||||||
|         "type": "github" |         "type": "github" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  | @ -119,36 +114,11 @@ | ||||||
|         ] |         ] | ||||||
|       }, |       }, | ||||||
|       "locked": { |       "locked": { | ||||||
|         "lastModified": 1660358625, |         "lastModified": 1663297375, | ||||||
|         "narHash": "sha256-uv+ZtOAEeM5tw78CLdRQmbZyDZYc0piSflthG2kNnrc=", |         "narHash": "sha256-7pjd2x9fSXXynIzp9XiXjbYys7sR6MKCot/jfGL7dgE=", | ||||||
|         "owner": "oxalica", |         "owner": "oxalica", | ||||||
|         "repo": "rust-overlay", |         "repo": "rust-overlay", | ||||||
|         "rev": "18354cce8137aaef0d505d6f677e9bbdd542020d", |         "rev": "0678b6187a153eb0baa9688335b002fe14ba6712", | ||||||
|         "type": "github" |  | ||||||
|       }, |  | ||||||
|       "original": { |  | ||||||
|         "owner": "oxalica", |  | ||||||
|         "repo": "rust-overlay", |  | ||||||
|         "type": "github" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "rust-overlay_2": { |  | ||||||
|       "inputs": { |  | ||||||
|         "flake-utils": [ |  | ||||||
|           "tlaternet-templates", |  | ||||||
|           "flake-utils" |  | ||||||
|         ], |  | ||||||
|         "nixpkgs": [ |  | ||||||
|           "tlaternet-templates", |  | ||||||
|           "nixpkgs" |  | ||||||
|         ] |  | ||||||
|       }, |  | ||||||
|       "locked": { |  | ||||||
|         "lastModified": 1633400100, |  | ||||||
|         "narHash": "sha256-kHQV7jZ2vVHVI9sfda1mUROVBbQbdfKcbIpKG9WdqGo=", |  | ||||||
|         "owner": "oxalica", |  | ||||||
|         "repo": "rust-overlay", |  | ||||||
|         "rev": "9c2fc6a62ccbc6f420d71ecac6bf0b84dbbee64f", |  | ||||||
|         "type": "github" |         "type": "github" | ||||||
|       }, |       }, | ||||||
|       "original": { |       "original": { | ||||||
|  | @ -159,23 +129,22 @@ | ||||||
|     }, |     }, | ||||||
|     "tlaternet-templates": { |     "tlaternet-templates": { | ||||||
|       "inputs": { |       "inputs": { | ||||||
|         "flake-utils": "flake-utils_2", |         "nix-filter": "nix-filter", | ||||||
|         "naersk": "naersk_2", |  | ||||||
|         "nixpkgs": "nixpkgs_2", |         "nixpkgs": "nixpkgs_2", | ||||||
|         "rust-overlay": "rust-overlay_2" |         "npmlock2nix": "npmlock2nix" | ||||||
|       }, |       }, | ||||||
|       "locked": { |       "locked": { | ||||||
|         "lastModified": 1633433130, |         "lastModified": 1663345814, | ||||||
|         "narHash": "sha256-jkW+HV8cJvE86gppOEXQl2ke+bDHJ7SAp8eJp8pJ0N8=", |         "narHash": "sha256-wIl8P+Hv8zHwBATlEoppPNJMpcR2EiQ4dbkgGXszmf8=", | ||||||
|         "ref": "master", |         "ref": "master", | ||||||
|         "rev": "1232950c06ae16bf17fb16ac1f5f2231e971936b", |         "rev": "789431c13cf1e906cbaf48e9b1078056c8ec3cc8", | ||||||
|         "revCount": 16, |         "revCount": 111, | ||||||
|         "type": "git", |         "type": "git", | ||||||
|         "url": "https://gitea.tlater.net/tlaternet/tlaternet.git" |         "url": "https://gitea.tlater.net/tlaternet/tlaternet-templates.git" | ||||||
|       }, |       }, | ||||||
|       "original": { |       "original": { | ||||||
|         "type": "git", |         "type": "git", | ||||||
|         "url": "https://gitea.tlater.net/tlaternet/tlaternet.git" |         "url": "https://gitea.tlater.net/tlaternet/tlaternet-templates.git" | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|  | @ -13,7 +13,7 @@ | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     tlaternet-templates = { |     tlaternet-templates = { | ||||||
|       url = "git+https://gitea.tlater.net/tlaternet/tlaternet.git"; |       url = "git+https://gitea.tlater.net/tlaternet/tlaternet-templates.git"; | ||||||
|       # No need to override anything here; we can save some downloads |       # No need to override anything here; we can save some downloads | ||||||
|       # if we rely on the webserver to do that. |       # if we rely on the webserver to do that. | ||||||
|     }; |     }; | ||||||
|  | @ -57,8 +57,8 @@ | ||||||
|     apps.${system} = { |     apps.${system} = { | ||||||
|       run-with-templates = let |       run-with-templates = let | ||||||
|         script = pkgs.writeShellScriptBin "run-with-templates" '' |         script = pkgs.writeShellScriptBin "run-with-templates" '' | ||||||
|           export ROCKET_TEMPLATE_DIR=${tlaternet-templates.packages.${system}.tlaternet-templates} |           RUST_LOG=info ${self.packages.${system}.tlaternet-webserver}/bin/tlaternet-webserver \ | ||||||
|           ${self.packages.${system}.tlaternet-webserver}/bin/tlaternet-webserver |               --template-directory ${tlaternet-templates.packages.${system}.default} | ||||||
|         ''; |         ''; | ||||||
|       in { |       in { | ||||||
|         type = "app"; |         type = "app"; | ||||||
|  |  | ||||||
|  | @ -1,38 +0,0 @@ | ||||||
| use rocket::request::FlashMessage; |  | ||||||
| 
 |  | ||||||
| use serde::Serialize; |  | ||||||
| 
 |  | ||||||
| #[derive(Default, Serialize)] |  | ||||||
| pub(crate) struct Context { |  | ||||||
|     pub flash: Option<Flash>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[allow(dead_code)] // Want to keep all possible flash types
 |  | ||||||
| #[derive(Serialize)] |  | ||||||
| #[serde(rename_all = "lowercase", tag = "type", content = "message")] |  | ||||||
| pub(crate) enum Flash { |  | ||||||
|     Primary(String), |  | ||||||
|     Secondary(String), |  | ||||||
|     Success(String), |  | ||||||
|     Danger(String), |  | ||||||
|     Warning(String), |  | ||||||
|     Info(String), |  | ||||||
|     Light(String), |  | ||||||
|     Dark(String), |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl From<FlashMessage<'_, '_>> for Flash { |  | ||||||
|     fn from(message: FlashMessage) -> Self { |  | ||||||
|         match message.name() { |  | ||||||
|             "success" => Self::Success(message.msg().to_string()), |  | ||||||
|             "warning" => Self::Warning(message.msg().to_string()), |  | ||||||
|             "error" => Self::Danger(message.msg().to_string()), |  | ||||||
|             "primary" => Self::Primary(message.msg().to_string()), |  | ||||||
|             "secondary" => Self::Secondary(message.msg().to_string()), |  | ||||||
|             "info" => Self::Info(message.msg().to_string()), |  | ||||||
|             "light" => Self::Light(message.msg().to_string()), |  | ||||||
|             "dark" => Self::Dark(message.msg().to_string()), |  | ||||||
|             name => Self::Info(format!("{}: {}", name, message.msg())), |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										72
									
								
								src/errors.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								src/errors.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,72 @@ | ||||||
|  | use actix_web::body::BoxBody; | ||||||
|  | use actix_web::dev::ServiceResponse; | ||||||
|  | use actix_web::http::header::ContentType; | ||||||
|  | use actix_web::http::StatusCode; | ||||||
|  | use actix_web::middleware::ErrorHandlerResponse; | ||||||
|  | use actix_web::{web, HttpResponse, ResponseError}; | ||||||
|  | use derive_more::{Display, Error}; | ||||||
|  | 
 | ||||||
|  | use super::SharedData; | ||||||
|  | use crate::template_utils::{render_template, ErrorMessage, TemplateArgs}; | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Display, Error)] | ||||||
|  | pub enum UserError { | ||||||
|  |     NotFound, | ||||||
|  |     #[display(fmt = "Internal error. Try again later.")] | ||||||
|  |     InternalError, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl ResponseError for UserError { | ||||||
|  |     fn error_response(&self) -> HttpResponse { | ||||||
|  |         HttpResponse::build(self.status_code()) | ||||||
|  |             .insert_header(ContentType::html()) | ||||||
|  |             .body(self.to_string()) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn status_code(&self) -> StatusCode { | ||||||
|  |         match *self { | ||||||
|  |             UserError::NotFound => StatusCode::NOT_FOUND, | ||||||
|  |             UserError::InternalError => StatusCode::INTERNAL_SERVER_ERROR, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub fn generic_error<B>( | ||||||
|  |     res: ServiceResponse<B>, | ||||||
|  | ) -> actix_web::Result<ErrorHandlerResponse<BoxBody>> { | ||||||
|  |     let data = res | ||||||
|  |         .request() | ||||||
|  |         .app_data::<web::Data<SharedData>>() | ||||||
|  |         .map(|t| t.get_ref()); | ||||||
|  | 
 | ||||||
|  |     let status_code = res.response().status(); | ||||||
|  |     let message = if let Some(error) = status_code.canonical_reason() { | ||||||
|  |         error | ||||||
|  |     } else { | ||||||
|  |         "" | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     let response = match data { | ||||||
|  |         Some(SharedData { | ||||||
|  |             handlebars, | ||||||
|  |             config: _, | ||||||
|  |         }) => { | ||||||
|  |             let args = TemplateArgs::builder() | ||||||
|  |                 .error_page(ErrorMessage::new(message, status_code.as_u16())) | ||||||
|  |                 .build(); | ||||||
|  | 
 | ||||||
|  |             let body = render_template(handlebars, "error", &args) | ||||||
|  |                 .map_err(|_| UserError::InternalError)?; | ||||||
|  | 
 | ||||||
|  |             HttpResponse::build(res.status()) | ||||||
|  |                 .content_type(ContentType::html()) | ||||||
|  |                 .body(body) | ||||||
|  |         } | ||||||
|  |         None => Err(UserError::InternalError)?, | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     Ok(ErrorHandlerResponse::Response(ServiceResponse::new( | ||||||
|  |         res.into_parts().0, | ||||||
|  |         response.map_into_left_body(), | ||||||
|  |     ))) | ||||||
|  | } | ||||||
							
								
								
									
										50
									
								
								src/mail.rs
									
										
									
									
									
								
							
							
						
						
									
										50
									
								
								src/mail.rs
									
										
									
									
									
								
							|  | @ -1,50 +0,0 @@ | ||||||
| use std::fs::create_dir_all; |  | ||||||
| 
 |  | ||||||
| use lettre::{FileTransport, Transport}; |  | ||||||
| use lettre_email::EmailBuilder; |  | ||||||
| use rocket::request::{Form, FromForm}; |  | ||||||
| use rocket::response::{Flash, Redirect}; |  | ||||||
| use rocket::{post, routes, Route}; |  | ||||||
| 
 |  | ||||||
| #[derive(FromForm)] |  | ||||||
| struct Email { |  | ||||||
|     mail: String, |  | ||||||
|     subject: String, |  | ||||||
|     message: String, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| fn send_mail(email: &Email) -> Result<(), String> { |  | ||||||
|     let email = EmailBuilder::new() |  | ||||||
|         .to("tm@tlater.net") |  | ||||||
|         .from(email.mail.clone()) |  | ||||||
|         .subject(email.subject.clone()) |  | ||||||
|         .text(email.message.clone()) |  | ||||||
|         .build() |  | ||||||
|         .map_err(|err| format!("Invalid email contents: {}", err))?; |  | ||||||
| 
 |  | ||||||
|     let mut mailer = FileTransport::new("/srv/mail"); |  | ||||||
|     if let Err(error) = create_dir_all("/srv/mail") { |  | ||||||
|         println!("Could not create mail directory: {}", error); |  | ||||||
|     }; |  | ||||||
|     mailer.send(email.into()).map_err(|err| { |  | ||||||
|         println!("Could not save mail: {}", err); |  | ||||||
|         "Failed to send email due to internal issues; please try again later".to_string() |  | ||||||
|     })?; |  | ||||||
| 
 |  | ||||||
|     Ok(()) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[post("/mail.html", data = "<email>")] |  | ||||||
| fn mail_post(email: Form<Email>) -> Result<Flash<Redirect>, Flash<Redirect>> { |  | ||||||
|     match send_mail(&email) { |  | ||||||
|         Ok(_) => Ok(Flash::success( |  | ||||||
|             Redirect::to("/mail.html"), |  | ||||||
|             "Email sent successfully", |  | ||||||
|         )), |  | ||||||
|         Err(err) => Err(Flash::error(Redirect::to("/mail.html"), err)), |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| pub fn routes() -> Vec<Route> { |  | ||||||
|     routes![mail_post] |  | ||||||
| } |  | ||||||
							
								
								
									
										107
									
								
								src/main.rs
									
										
									
									
									
								
							
							
						
						
									
										107
									
								
								src/main.rs
									
										
									
									
									
								
							|  | @ -1,34 +1,81 @@ | ||||||
| #![feature(proc_macro_hygiene, decl_macro)] | #![allow(dead_code)] | ||||||
|  | use std::net::SocketAddr; | ||||||
|  | use std::path::PathBuf; | ||||||
| 
 | 
 | ||||||
| use rocket::fairing::AdHoc; | use actix_files::Files; | ||||||
| 
 | use actix_web::{ | ||||||
| use rocket_contrib::serve::StaticFiles; |     http::{Method, StatusCode}, | ||||||
| use rocket_contrib::templates::Template; |     middleware::{self, ErrorHandlers}, | ||||||
| 
 |     web, App, HttpServer, | ||||||
| mod context; |  | ||||||
| mod mail; |  | ||||||
| mod static_templates; |  | ||||||
| 
 |  | ||||||
| use mail::routes as mail_routes; |  | ||||||
| use static_templates::routes as static_templates; |  | ||||||
| 
 |  | ||||||
| fn main() { |  | ||||||
|     rocket::ignite() |  | ||||||
|         .attach(Template::fairing()) |  | ||||||
|         .mount("/", mail_routes()) |  | ||||||
|         .mount("/", static_templates()) |  | ||||||
|         .attach(AdHoc::on_attach("Static files config", |rocket| { |  | ||||||
|             let static_path = match rocket.config().get_string("template_dir") { |  | ||||||
|                 Ok(dir) => dir, |  | ||||||
|                 Err(rocket::config::ConfigError::Missing { .. }) => "templates".to_string(), |  | ||||||
|                 Err(err) => { |  | ||||||
|                     eprintln!("Error reading configuration: {}", err); |  | ||||||
|                     eprintln!("Using default templates path."); |  | ||||||
|                     "templates".to_string() |  | ||||||
|                 }, |  | ||||||
| }; | }; | ||||||
|  | use clap::Parser; | ||||||
|  | use handlebars::Handlebars; | ||||||
| 
 | 
 | ||||||
|             Ok(rocket.mount("/", StaticFiles::from(static_path))) | mod errors; | ||||||
|         })) | mod main_pages; | ||||||
|         .launch(); | mod template_utils; | ||||||
|  | 
 | ||||||
|  | use errors::generic_error; | ||||||
|  | use main_pages::{mail_post, template}; | ||||||
|  | 
 | ||||||
|  | #[derive(Parser, Debug, Clone)] | ||||||
|  | struct Config { | ||||||
|  |     #[clap(long, value_parser)] | ||||||
|  |     /// The directory from which to serve static content and
 | ||||||
|  |     /// handlebars templates
 | ||||||
|  |     template_directory: PathBuf, | ||||||
|  |     #[clap(long, default_value = "127.0.0.1:8000", value_parser)] | ||||||
|  |     /// The address on which to listen
 | ||||||
|  |     address: SocketAddr, | ||||||
|  |     #[clap(long, action)] | ||||||
|  |     /// Whether to start the server in dev mode; this enables some
 | ||||||
|  |     /// nice handlebars features that are not intended for production
 | ||||||
|  |     dev_mode: bool, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug)] | ||||||
|  | struct SharedData<'a> { | ||||||
|  |     handlebars: Handlebars<'a>, | ||||||
|  |     config: Config, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[actix_web::main] | ||||||
|  | async fn main() -> std::io::Result<()> { | ||||||
|  |     let mut config = Config::parse(); | ||||||
|  |     config.template_directory = config.template_directory.canonicalize()?; | ||||||
|  | 
 | ||||||
|  |     env_logger::init(); | ||||||
|  | 
 | ||||||
|  |     let mut handlebars = Handlebars::new(); | ||||||
|  |     handlebars | ||||||
|  |         .register_templates_directory(".html", config.template_directory.clone()) | ||||||
|  |         .expect("templates should compile correctly"); | ||||||
|  |     handlebars.set_dev_mode(config.dev_mode); | ||||||
|  | 
 | ||||||
|  |     let shared_data = web::Data::new(SharedData { | ||||||
|  |         handlebars, | ||||||
|  |         config: config.clone(), | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     HttpServer::new(move || { | ||||||
|  |         App::new() | ||||||
|  |             .wrap(middleware::NormalizePath::trim()) | ||||||
|  |             // TODO(tlater): When actix-web 4.3 releases, this can be improved a
 | ||||||
|  |             // lot because of this PR:
 | ||||||
|  |             //
 | ||||||
|  |             // https://github.com/actix/actix-web/pull/2784
 | ||||||
|  |             .wrap( | ||||||
|  |                 ErrorHandlers::new() | ||||||
|  |                     .handler(StatusCode::NOT_FOUND, generic_error) | ||||||
|  |                     .handler(StatusCode::INTERNAL_SERVER_ERROR, generic_error), | ||||||
|  |             ) | ||||||
|  |             .app_data(shared_data.clone()) | ||||||
|  |             .service(template) | ||||||
|  |             .service(mail_post) | ||||||
|  |             .service(Files::new("/", &config.template_directory)) | ||||||
|  |             .default_service(web::route().method(Method::GET)) | ||||||
|  |     }) | ||||||
|  |     .bind(config.address)? | ||||||
|  |     .run() | ||||||
|  |     .await | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										45
									
								
								src/main_pages.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/main_pages.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,45 @@ | ||||||
|  | use actix_web::{post, routes, web, HttpRequest, HttpResponse, Responder}; | ||||||
|  | use log::info; | ||||||
|  | use serde::Deserialize; | ||||||
|  | 
 | ||||||
|  | use crate::template_utils::{render_template, Flash, FlashType, TemplateArgs}; | ||||||
|  | use crate::SharedData; | ||||||
|  | 
 | ||||||
|  | #[derive(Clone, Debug, Deserialize)] | ||||||
|  | pub(crate) struct Mail { | ||||||
|  |     mail: String, | ||||||
|  |     subject: String, | ||||||
|  |     message: String, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[routes] | ||||||
|  | #[get(r"/")] | ||||||
|  | #[get(r"/{filename:.*\.html}")] | ||||||
|  | pub(crate) async fn template( | ||||||
|  |     shared: web::Data<SharedData<'_>>, | ||||||
|  |     req: HttpRequest, | ||||||
|  | ) -> actix_web::Result<impl Responder> { | ||||||
|  |     let path = match req.match_info().query("filename") { | ||||||
|  |         "" => "index", | ||||||
|  |         other => other | ||||||
|  |             .strip_suffix(".html") | ||||||
|  |             .expect("only paths with this suffix should get here"), | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     render_template(&shared.handlebars, path, &TemplateArgs::default()) | ||||||
|  |         .map(|body| HttpResponse::Ok().body(body)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[post("/mail.html")] | ||||||
|  | pub(crate) async fn mail_post( | ||||||
|  |     shared: web::Data<SharedData<'_>>, | ||||||
|  |     form: web::Form<Mail>, | ||||||
|  | ) -> actix_web::Result<impl Responder> { | ||||||
|  |     info!("{:?}", form); | ||||||
|  | 
 | ||||||
|  |     let args = TemplateArgs::builder() | ||||||
|  |         .flash(Flash::new("Mail successfully sent!", FlashType::Success)) | ||||||
|  |         .build(); | ||||||
|  | 
 | ||||||
|  |     render_template(&shared.handlebars, "mail", &args).map(|body| HttpResponse::Ok().body(body)) | ||||||
|  | } | ||||||
|  | @ -1,78 +0,0 @@ | ||||||
| /// Serve templates "statically".
 |  | ||||||
| ///
 |  | ||||||
| /// In the generic case, we don't want to do any processing on
 |  | ||||||
| /// template pages. They should be served "statically", but we want to
 |  | ||||||
| /// transform the bits of default handlebars templating - currently
 |  | ||||||
| /// setting the flash to nothing.
 |  | ||||||
| ///
 |  | ||||||
| /// This module implements a catchall route for this purpose.
 |  | ||||||
| use rocket::request::FlashMessage; |  | ||||||
| use std::ffi::OsString; |  | ||||||
| 
 |  | ||||||
| use rocket::http::uri::Segments; |  | ||||||
| use rocket::request::FromSegments; |  | ||||||
| use rocket::response::status; |  | ||||||
| use rocket::{get, routes, Route}; |  | ||||||
| use rocket_contrib::templates::Template; |  | ||||||
| 
 |  | ||||||
| use crate::context::Context; |  | ||||||
| 
 |  | ||||||
| pub struct HTMLPage { |  | ||||||
|     pub page: OsString, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl<'a> FromSegments<'a> for HTMLPage { |  | ||||||
|     type Error = &'static str; |  | ||||||
| 
 |  | ||||||
|     fn from_segments(segments: Segments<'a>) -> Result<Self, Self::Error> { |  | ||||||
|         let page = segments |  | ||||||
|             .into_path_buf(false) |  | ||||||
|             .map_err(|_| "Invalid segments")?; |  | ||||||
| 
 |  | ||||||
|         page.extension() |  | ||||||
|             .map(|extension| { |  | ||||||
|                 if extension == "html" { |  | ||||||
|                     Some(HTMLPage { |  | ||||||
|                         page: page |  | ||||||
|                             .file_stem() |  | ||||||
|                             .expect("Should exist if the extension does") |  | ||||||
|                             .to_os_string(), |  | ||||||
|                     }) |  | ||||||
|                 } else { |  | ||||||
|                     None |  | ||||||
|                 } |  | ||||||
|             }) |  | ||||||
|             .flatten() |  | ||||||
|             .ok_or("Invalid page name") |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[get("/")] |  | ||||||
| pub fn static_index() -> Template { |  | ||||||
|     Template::render("index", Context::default()) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[get("/<path..>")] |  | ||||||
| pub fn static_templates( |  | ||||||
|     path: HTMLPage, |  | ||||||
|     flash: Option<FlashMessage>, |  | ||||||
| ) -> Result<Template, status::BadRequest<String>> { |  | ||||||
|     let path = path.page.into_string().map_err(|path| { |  | ||||||
|         status::BadRequest(Some(format!("Invalid path: {}", path.to_string_lossy()))) |  | ||||||
|     })?; |  | ||||||
| 
 |  | ||||||
|     if let Some(flash) = flash { |  | ||||||
|         Ok(Template::render( |  | ||||||
|             path, |  | ||||||
|             Context { |  | ||||||
|                 flash: Some(flash.into()), |  | ||||||
|             }, |  | ||||||
|         )) |  | ||||||
|     } else { |  | ||||||
|         Ok(Template::render(path, Context::default())) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| pub fn routes() -> Vec<Route> { |  | ||||||
|     routes![static_index, static_templates] |  | ||||||
| } |  | ||||||
							
								
								
									
										105
									
								
								src/template_utils.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								src/template_utils.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,105 @@ | ||||||
|  | use log::error; | ||||||
|  | use serde::Serialize; | ||||||
|  | 
 | ||||||
|  | use crate::errors::UserError; | ||||||
|  | 
 | ||||||
|  | pub fn render_template( | ||||||
|  |     handlebars: &handlebars::Handlebars, | ||||||
|  |     name: &str, | ||||||
|  |     args: &TemplateArgs, | ||||||
|  | ) -> actix_web::Result<String> { | ||||||
|  |     if handlebars.has_template(name) { | ||||||
|  |         Ok(handlebars | ||||||
|  |             .render(name, args) | ||||||
|  |             .map_err(|_| UserError::InternalError)?) | ||||||
|  |     } else { | ||||||
|  |         error!("template not found: {}", name); | ||||||
|  |         Err(UserError::NotFound)? | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** All arguments that can be given to a template. */ | ||||||
|  | #[derive(Default, Serialize)] | ||||||
|  | pub struct TemplateArgs { | ||||||
|  |     flash: Option<Flash>, | ||||||
|  |     error: Option<ErrorMessage>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl TemplateArgs { | ||||||
|  |     pub fn builder() -> TemplateArgsBuilder { | ||||||
|  |         TemplateArgsBuilder::new() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub struct TemplateArgsBuilder { | ||||||
|  |     flash: Option<Flash>, | ||||||
|  |     error: Option<ErrorMessage>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl TemplateArgsBuilder { | ||||||
|  |     pub fn new() -> Self { | ||||||
|  |         TemplateArgsBuilder { | ||||||
|  |             flash: None, | ||||||
|  |             error: None, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn flash(mut self, flash: Flash) -> Self { | ||||||
|  |         self.flash = Some(flash); | ||||||
|  |         self | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn error_page(mut self, error: ErrorMessage) -> Self { | ||||||
|  |         self.error = Some(error); | ||||||
|  |         self | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn build(self) -> TemplateArgs { | ||||||
|  |         TemplateArgs { | ||||||
|  |             flash: self.flash, | ||||||
|  |             error: self.error, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** A flash message that should be displayed as a notification on the page. */ | ||||||
|  | #[derive(Serialize)] | ||||||
|  | pub struct Flash { | ||||||
|  |     message: String, | ||||||
|  |     #[serde(rename = "type")] | ||||||
|  |     level: FlashType, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Flash { | ||||||
|  |     pub fn new(message: &str, level: FlashType) -> Self { | ||||||
|  |         Self { | ||||||
|  |             message: message.to_string(), | ||||||
|  |             level, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Serialize)] | ||||||
|  | #[serde(rename_all = "lowercase")] | ||||||
|  | pub enum FlashType { | ||||||
|  |     Info, | ||||||
|  |     Success, | ||||||
|  |     Warning, | ||||||
|  |     Danger, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Contents of an error page. */ | ||||||
|  | #[derive(Serialize)] | ||||||
|  | pub struct ErrorMessage { | ||||||
|  |     message: String, | ||||||
|  |     status_code: u16, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl ErrorMessage { | ||||||
|  |     pub fn new(message: &str, status_code: u16) -> Self { | ||||||
|  |         Self { | ||||||
|  |             message: message.to_string(), | ||||||
|  |             status_code, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue