treewide: Migrate to the new dream2nix API

This commit is contained in:
Tristan Daniël Maat 2024-01-01 19:56:16 +01:00
parent 5163ef9d6a
commit 40e0946201
Signed by: tlater
GPG key ID: 49670FD774E43268
59 changed files with 15003 additions and 24504 deletions

View file

@ -0,0 +1,4 @@
((rust-mode . ((indent-tabs-mode . nil)
(tab-width . 4)
(fill-column . 80)
(projectile-project-run-cmd . "cd server && cargo run -- --dev-mode --template-directory ../templates/result"))))

1515
packages/server/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,15 @@
[package]
name = "tlaternet-webserver"
version = "0.1.0"
edition = "2021"
[dependencies]
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"

View file

@ -0,0 +1,17 @@
{dream2nix, ...}: {
imports = [
dream2nix.modules.dream2nix.rust-cargo-lock
dream2nix.modules.dream2nix.rust-crane
];
deps = {fenix, ...}: {
deps.cargo = fenix.stable.minimalToolchain;
};
name = "tlaternet-webserver";
version = "0.1.0";
mkDerivation = {
src = ./.;
};
}

View 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(),
)))
}

View file

@ -0,0 +1,99 @@
#![allow(dead_code)]
use std::net::SocketAddr;
use std::path::PathBuf;
use actix_files::Files;
use actix_web::{
http::{Method, StatusCode},
middleware::{self, ErrorHandlers},
web, App, HttpServer,
};
use clap::Parser;
use env_logger::{Env, WriteStyle};
use handlebars::Handlebars;
use log::LevelFilter;
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, and enables
/// more verbose logs
dev_mode: bool,
}
#[derive(Debug)]
struct SharedData<'a> {
handlebars: Handlebars<'a>,
config: Config,
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let config = {
let mut config = Config::parse();
config.template_directory = config.template_directory.canonicalize()?;
config
};
env_logger::Builder::new()
.filter_level(if config.dev_mode {
LevelFilter::Info
} else {
LevelFilter::Debug
})
.write_style(WriteStyle::Always)
.parse_env(
Env::new()
.filter("TLATERNET_LOG_LEVEL")
.write_style("TLATERNET_LOG_STYLE"),
)
.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
}

View 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))
}

View 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,
}
}
}