Migrate to actix-web #8
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]
|
||||
name = "tlaternet-webserver"
|
||||
version = "0.1.0"
|
||||
authors = ["Tristan Daniël Maat <tm@tlater.net>"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
check-if-email-exists = "0.8.5"
|
||||
lettre = "0.9.3"
|
||||
lettre_email = "0.9.3"
|
||||
rocket = "0.4.4"
|
||||
rocket_contrib = { version = "0.4.4", features = [ "handlebars_templates", "serve" ] }
|
||||
serde = { version = "1.0.111", features = [ "derive" ] }
|
||||
actix-files = "0.6.2"
|
||||
actix-web = { version = "4.2.1", features = ["macros"] }
|
||||
clap = { version = "3.2.17", features = ["derive"] }
|
||||
derive_more = "0.99.17"
|
||||
env_logger = "0.9.0"
|
||||
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
125
flake.lock
|
@ -15,21 +15,6 @@
|
|||
"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": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
|
@ -37,11 +22,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1659610603,
|
||||
"narHash": "sha256-LYgASYSPYo7O71WfeUOaEUzYfzuXm8c8eavJcel+pfI=",
|
||||
"lastModified": 1662220400,
|
||||
"narHash": "sha256-9o2OGQqu4xyLZP9K6kNe1pTHnyPz0Wr3raGYnr9AIgY=",
|
||||
"owner": "nmattia",
|
||||
"repo": "naersk",
|
||||
"rev": "c6a45e4277fa58abd524681466d3450f896dc094",
|
||||
"rev": "6944160c19cb591eb85bbf9b2f2768a935623ed3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -50,28 +35,38 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"naersk_2": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"tlaternet-templates",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"nix-filter": {
|
||||
"locked": {
|
||||
"lastModified": 1632266297,
|
||||
"narHash": "sha256-J1yeJk6Gud9ef2pEf6aKQemrfg1pVngYDSh+SAY94xk=",
|
||||
"owner": "nmattia",
|
||||
"repo": "naersk",
|
||||
"rev": "ee7edec50b49ab6d69b06d62f1de554efccb1ccd",
|
||||
"lastModified": 1661201956,
|
||||
"narHash": "sha256-RizGJH/buaw9A2+fiBf9WnXYw4LZABB5kMAZIEE5/T8=",
|
||||
"owner": "numtide",
|
||||
"repo": "nix-filter",
|
||||
"rev": "3b821578685d661a10b563cba30b1861eec05748",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nmattia",
|
||||
"repo": "naersk",
|
||||
"owner": "numtide",
|
||||
"repo": "nix-filter",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"lastModified": 1660318005,
|
||||
"narHash": "sha256-g9WCa9lVUmOV6dYRbEPjv/TLOR5hamjeCcKExVGS3OQ=",
|
||||
|
@ -87,19 +82,19 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"npmlock2nix": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1632408697,
|
||||
"narHash": "sha256-JqTfu361AwFmV0WszXLAjfukqGxBbHRopRgdp9A2w8s=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a13824fe5e38187fbc75cd598b5c06bdcc13501f",
|
||||
"lastModified": 1654775747,
|
||||
"narHash": "sha256-9pXHDpIjmsK5390wmpGHu9aA4QOPpegPBvThHeBlef4=",
|
||||
"owner": "nix-community",
|
||||
"repo": "npmlock2nix",
|
||||
"rev": "5c4f247688fc91d665df65f71c81e0726621aaa8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-20.09",
|
||||
"repo": "nixpkgs",
|
||||
"owner": "nix-community",
|
||||
"repo": "npmlock2nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
|
@ -119,36 +114,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1660358625,
|
||||
"narHash": "sha256-uv+ZtOAEeM5tw78CLdRQmbZyDZYc0piSflthG2kNnrc=",
|
||||
"lastModified": 1663297375,
|
||||
"narHash": "sha256-7pjd2x9fSXXynIzp9XiXjbYys7sR6MKCot/jfGL7dgE=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "18354cce8137aaef0d505d6f677e9bbdd542020d",
|
||||
"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",
|
||||
"rev": "0678b6187a153eb0baa9688335b002fe14ba6712",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -159,23 +129,22 @@
|
|||
},
|
||||
"tlaternet-templates": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_2",
|
||||
"naersk": "naersk_2",
|
||||
"nix-filter": "nix-filter",
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"rust-overlay": "rust-overlay_2"
|
||||
"npmlock2nix": "npmlock2nix"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1633433130,
|
||||
"narHash": "sha256-jkW+HV8cJvE86gppOEXQl2ke+bDHJ7SAp8eJp8pJ0N8=",
|
||||
"lastModified": 1663345814,
|
||||
"narHash": "sha256-wIl8P+Hv8zHwBATlEoppPNJMpcR2EiQ4dbkgGXszmf8=",
|
||||
"ref": "master",
|
||||
"rev": "1232950c06ae16bf17fb16ac1f5f2231e971936b",
|
||||
"revCount": 16,
|
||||
"rev": "789431c13cf1e906cbaf48e9b1078056c8ec3cc8",
|
||||
"revCount": 111,
|
||||
"type": "git",
|
||||
"url": "https://gitea.tlater.net/tlaternet/tlaternet.git"
|
||||
"url": "https://gitea.tlater.net/tlaternet/tlaternet-templates.git"
|
||||
},
|
||||
"original": {
|
||||
"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 = {
|
||||
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
|
||||
# if we rely on the webserver to do that.
|
||||
};
|
||||
|
@ -57,8 +57,8 @@
|
|||
apps.${system} = {
|
||||
run-with-templates = let
|
||||
script = pkgs.writeShellScriptBin "run-with-templates" ''
|
||||
export ROCKET_TEMPLATE_DIR=${tlaternet-templates.packages.${system}.tlaternet-templates}
|
||||
${self.packages.${system}.tlaternet-webserver}/bin/tlaternet-webserver
|
||||
RUST_LOG=info ${self.packages.${system}.tlaternet-webserver}/bin/tlaternet-webserver \
|
||||
--template-directory ${tlaternet-templates.packages.${system}.default}
|
||||
'';
|
||||
in {
|
||||
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 rocket_contrib::serve::StaticFiles;
|
||||
use rocket_contrib::templates::Template;
|
||||
|
||||
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 actix_files::Files;
|
||||
use actix_web::{
|
||||
http::{Method, StatusCode},
|
||||
middleware::{self, ErrorHandlers},
|
||||
web, App, HttpServer,
|
||||
};
|
||||
use clap::Parser;
|
||||
use handlebars::Handlebars;
|
||||
|
||||
Ok(rocket.mount("/", StaticFiles::from(static_path)))
|
||||
}))
|
||||
.launch();
|
||||
mod errors;
|
||||
mod main_pages;
|
||||
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…
Reference in a new issue