diff --git a/src/Cargo.lock b/src/Cargo.lock index 8f8a949..d4c56e6 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -191,6 +191,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.5.0" @@ -215,6 +221,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bstr" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -259,6 +275,28 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "chrono-tz" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59ae0466b83e838b81a54256c39d5d7c20b9d7daa10510a242d9b75abd5936e" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433e39f13c9a060046954e0592a8d0a4bcb1040125cbf91cb8ee58964cfb350f" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -291,6 +329,31 @@ dependencies = [ "libc", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + [[package]] name = "crypto-common" version = "0.1.6" @@ -348,6 +411,12 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "deunicode" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322ef0094744e63628e6f0eb2295517f79276a5b342a4c2ff3042566ca181d4e" + [[package]] name = "devise" version = "0.4.1" @@ -374,7 +443,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35b50dba0afdca80b187392b24f2499a88c336d5a8493e4b4ccfb608708be56a" dependencies = [ - "bitflags", + "bitflags 2.5.0", "proc-macro2", "proc-macro2-diagnostics", "quote", @@ -610,6 +679,30 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "globset" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata 0.4.6", + "regex-syntax 0.8.3", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags 1.3.2", + "ignore", + "walkdir", +] + [[package]] name = "h2" version = "0.3.26" @@ -692,6 +785,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + [[package]] name = "hyper" version = "0.14.28" @@ -739,6 +841,22 @@ dependencies = [ "cc", ] +[[package]] +name = "ignore" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata 0.4.6", + "same-file", + "walkdir", + "winapi-util", +] + [[package]] name = "indexmap" version = "2.2.6" @@ -812,6 +930,12 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -1021,15 +1145,17 @@ dependencies = [ [[package]] name = "orangutan-server" -version = "0.4.5" +version = "0.4.6" dependencies = [ "base64 0.22.1", "biscuit-auth", "chrono", "lazy_static", "orangutan-helpers", + "orangutan-refresh-token", "rocket", "serde_json", + "tera", "thiserror", "time", "tracing", @@ -1066,6 +1192,15 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + [[package]] name = "pear" version = "0.2.9" @@ -1095,6 +1230,89 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pest" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "pest_meta" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd" +dependencies = [ + "once_cell", + "pest", + "sha2 0.10.8", +] + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.14" @@ -1259,7 +1477,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" dependencies = [ - "bitflags", + "bitflags 2.5.0", ] [[package]] @@ -1428,7 +1646,7 @@ version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags", + "bitflags 2.5.0", "errno", "libc", "linux-raw-sys", @@ -1447,6 +1665,15 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -1556,6 +1783,12 @@ dependencies = [ "rand_core", ] +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "slab" version = "0.4.9" @@ -1565,6 +1798,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slug" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bd94acec9c8da640005f8e135a39fc0372e74535e6b368b7a04b875f784c8c4" +dependencies = [ + "deunicode", + "wasm-bindgen", +] + [[package]] name = "smallvec" version = "1.13.2" @@ -1655,6 +1898,28 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "tera" +version = "1.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "970dff17c11e884a4a09bc76e3a17ef71e01bb13447a11e85226e254fe6d10b8" +dependencies = [ + "chrono", + "chrono-tz", + "globwalk", + "humansize", + "lazy_static", + "percent-encoding", + "pest", + "pest_derive", + "rand", + "regex", + "serde", + "serde_json", + "slug", + "unic-segment", +] + [[package]] name = "thiserror" version = "1.0.61" @@ -1891,6 +2156,12 @@ dependencies = [ "serde", ] +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + [[package]] name = "uncased" version = "0.9.10" @@ -1901,6 +2172,56 @@ dependencies = [ "version_check", ] +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" +dependencies = [ + "unic-ucd-segment", +] + +[[package]] +name = "unic-ucd-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + [[package]] name = "unicode-ident" version = "1.0.12" @@ -1931,6 +2252,16 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -2022,6 +2353,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/src/Cargo.toml b/src/Cargo.toml index 808c145..7eaf9c0 100644 --- a/src/Cargo.toml +++ b/src/Cargo.toml @@ -16,6 +16,7 @@ iso8601-duration = "0.2" lazy_static = "1" rocket = "0.5" serde_json = "1" +tera = "1" thiserror = "1" time = "0.3" tracing = "0.1" diff --git a/src/orangutan-server/Cargo.toml b/src/orangutan-server/Cargo.toml index 8860eeb..216985e 100644 --- a/src/orangutan-server/Cargo.toml +++ b/src/orangutan-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "orangutan-server" -version = "0.4.5" +version = "0.4.6" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -11,10 +11,17 @@ biscuit-auth = { workspace = true } chrono = { workspace = true } lazy_static = { workspace = true } orangutan-helpers = { path = "../helpers" } +orangutan-refresh-token = { path = "../orangutan-refresh-token" } rocket = { workspace = true } serde_json = { workspace = true } +tera = { workspace = true, optional = true } thiserror = { workspace = true } time = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } urlencoding = { workspace = true } + +[features] +default = ["token-generator"] +templating = ["tera"] +token-generator = ["templating"] diff --git a/src/orangutan-server/src/main.rs b/src/orangutan-server/src/main.rs index 3460d33..5424502 100644 --- a/src/orangutan-server/src/main.rs +++ b/src/orangutan-server/src/main.rs @@ -17,15 +17,22 @@ use rocket::{ response::{self, Responder}, Request, }; -use routes::{main_route, update_content_routes}; +#[cfg(feature = "templating")] +use tracing::debug; +use tracing::warn; use tracing_subscriber::{EnvFilter, FmtSubscriber}; -use util::error; -use crate::config::NOT_FOUND_FILE; +#[cfg(feature = "templating")] +use crate::util::templating; +use crate::{ + config::NOT_FOUND_FILE, + routes::{main_route, update_content_routes}, + util::error, +}; #[rocket::launch] fn rocket() -> _ { - rocket::build() + let rocket = rocket::build() .mount("/", routes::routes()) .register("/", catchers![unauthorized, not_found]) .manage(ObjectReader::new()) @@ -45,7 +52,23 @@ fn rocket() -> _ { rocket.shutdown().notify(); } }) - })) + })); + + // Add support for templating if needed + #[cfg(feature = "templating")] + let rocket = rocket.attach(AdHoc::on_ignite( + "Initialize templating engine", + |rocket| async move { + let mut tera = tera::Tera::default(); + if let Err(err) = tera.add_raw_templates(routes::templates()) { + tracing::error!("{err}"); + std::process::exit(1) + } + rocket.manage(tera) + }, + )); + + rocket } fn liftoff() -> Result<(), Error> { @@ -91,6 +114,17 @@ enum Error { MainRouteError(#[from] main_route::Error), #[error("Could not update content: {0}")] UpdateContentError(#[from] update_content_routes::Error), + #[error("Unauthorized")] + Unauthorized, + #[cfg(feature = "templating")] + #[error("Templating error: {0}")] + TemplatingError(#[from] templating::Error), + #[cfg(feature = "templating")] + #[error("Internal server error: {0}")] + InternalServerError(String), + #[cfg(feature = "templating")] + #[error("Client error: {0}")] + ClientError(String), } #[rocket::async_trait] @@ -99,7 +133,38 @@ impl<'r> Responder<'r, 'static> for Error { self, _: &'r Request<'_>, ) -> response::Result<'static> { - error(format!("{self}")); - Err(Status::InternalServerError) + match self { + Self::Unauthorized => { + warn!("{self}"); + Err(Status::Unauthorized) + }, + #[cfg(feature = "templating")] + Self::ClientError(_) => { + debug!("{self}"); + Err(Status::BadRequest) + }, + _ => { + error(format!("{self}")); + Err(Status::InternalServerError) + }, + } + } +} + +#[cfg(feature = "templating")] +impl From for Error { + fn from(err: orangutan_refresh_token::Error) -> Self { + match err { + orangutan_refresh_token::Error::CannotAddFact(_, _) + | orangutan_refresh_token::Error::CannotBuildBiscuit(_) + | orangutan_refresh_token::Error::CannotAddBlock(_, _) + | orangutan_refresh_token::Error::CannotConvertToBase64(_) => { + Self::InternalServerError(format!("Token generation error: {err}")) + }, + orangutan_refresh_token::Error::MalformattedDuration(_, _) + | orangutan_refresh_token::Error::UnsupportedDuration(_) => { + Self::ClientError(format!("Invalid token data: {err}")) + }, + } } } diff --git a/src/orangutan-server/src/routes/debug_routes.rs b/src/orangutan-server/src/routes/debug_routes.rs index 66304a6..4ca681d 100644 --- a/src/orangutan-server/src/routes/debug_routes.rs +++ b/src/orangutan-server/src/routes/debug_routes.rs @@ -2,13 +2,9 @@ use std::sync::{Arc, RwLock}; use chrono::{DateTime, Utc}; use lazy_static::lazy_static; -use rocket::{ - get, - http::{CookieJar, Status}, - routes, Route, -}; +use rocket::{get, http::CookieJar, routes, Route}; -use crate::request_guards::Token; +use crate::{request_guards::Token, Error}; lazy_static! { /// A list of runtime errors, used to show error logs in an admin page @@ -23,12 +19,27 @@ lazy_static! { } pub(super) fn routes() -> Vec { - routes![ + let routes = routes![ clear_cookies, get_user_info, errors, - access_logs - ] + access_logs, + ]; + #[cfg(feature = "token-generator")] + let routes = vec![routes, routes![ + token_generator::token_generation_form, + token_generator::generate_token, + ]] + .concat(); + routes +} + +#[cfg(feature = "templating")] +pub(super) fn templates() -> Vec<(&'static str, &'static str)> { + vec![( + "generate-token.html", + include_str!("templates/generate-token.html.tera"), + )] } #[get("/clear-cookies")] @@ -61,9 +72,9 @@ pub struct ErrorLog { } #[get("/_errors")] -fn errors(token: Token) -> Result { +fn errors(token: Token) -> Result { if !token.profiles().contains(&"*".to_owned()) { - Err(Status::Unauthorized)? + Err(Error::Unauthorized)? } let mut res = String::new(); @@ -88,9 +99,9 @@ pub struct AccessLog { } #[get("/_access-logs")] -fn access_logs(token: Token) -> Result { +fn access_logs(token: Token) -> Result { if !token.profiles().contains(&"*".to_owned()) { - Err(Status::Unauthorized)? + Err(Error::Unauthorized)? } let mut res = String::new(); @@ -125,3 +136,82 @@ pub fn log_access( path, }) } + +#[cfg(feature = "token-generator")] +pub mod token_generator { + use orangutan_refresh_token::RefreshToken; + use rocket::{ + form::{Form, Strict}, + get, post, + response::content::RawHtml, + FromForm, State, + }; + + use crate::{ + context, + request_guards::Token, + util::{templating::render, WebsiteRoot}, + Error, + }; + + fn token_generation_form_( + tera: &State, + link: Option, + base_url: &str, + ) -> Result, Error> { + let html = render( + tera, + "generate-token.html", + context! { page_title: "Access token generator", link, base_url }, + )?; + + Ok(RawHtml(html)) + } + + #[get("/_generate-token")] + pub fn token_generation_form( + token: Token, + tera: &State, + website_root: WebsiteRoot, + ) -> Result, Error> { + if !token.profiles().contains(&"*".to_owned()) { + Err(Error::Unauthorized)? + } + + token_generation_form_(tera, None, &website_root) + } + + #[derive(FromForm)] + pub struct GenerateTokenForm { + ttl: String, + name: String, + profiles: String, + url: String, + } + + #[post("/_generate-token", data = "
")] + pub fn generate_token( + token: Token, + tera: &State, + form: Form>, + website_root: WebsiteRoot, + ) -> Result, Error> { + if !token.profiles().contains(&"*".to_owned()) { + Err(Error::Unauthorized)? + } + + let mut profiles = vec![form.name.to_owned()]; + profiles.append(&mut form.profiles.split(",").map(ToOwned::to_owned).collect()); + if profiles.contains(&"*".to_string()) { + Err(Error::ClientError(format!( + "Profiles cannot contain '*' (got {profiles:?})." + )))? + } + + let token = RefreshToken::try_from(form.ttl.to_owned(), profiles.into_iter())?; + let token_base64 = token.as_base64()?; + let link = format!("{}?refresh_token={token_base64}", form.url); + + token_generation_form_(tera, Some(link), &website_root) + } +} diff --git a/src/orangutan-server/src/routes/mod.rs b/src/orangutan-server/src/routes/mod.rs index dba0671..e59d060 100644 --- a/src/orangutan-server/src/routes/mod.rs +++ b/src/orangutan-server/src/routes/mod.rs @@ -19,3 +19,12 @@ pub(super) fn routes() -> Vec { ] .concat() } + +#[cfg(feature = "templating")] +pub(super) fn templates() -> Vec<(&'static str, &'static str)> { + vec![ + vec![("base.html", include_str!("templates/base.html.tera"))], + debug_routes::templates(), + ] + .concat() +} diff --git a/src/orangutan-server/src/routes/templates/.zed/settings.json b/src/orangutan-server/src/routes/templates/.zed/settings.json new file mode 100644 index 0000000..8ed7c36 --- /dev/null +++ b/src/orangutan-server/src/routes/templates/.zed/settings.json @@ -0,0 +1,7 @@ +// Folder-specific settings +// +// For a full list of overridable settings, and general information on folder-specific settings, +// see the documentation: https://zed.dev/docs/configuring-zed#folder-specific-settings +{ + "tab_size": 2 +} diff --git a/src/orangutan-server/src/routes/templates/base.html.tera b/src/orangutan-server/src/routes/templates/base.html.tera new file mode 100644 index 0000000..a99ba48 --- /dev/null +++ b/src/orangutan-server/src/routes/templates/base.html.tera @@ -0,0 +1,39 @@ + + + + + + {{ page_title }} + + + +

{{ page_title }}

+
+ {% block main %}{% endblock main %} +
+ + diff --git a/src/orangutan-server/src/routes/templates/generate-token.html.tera b/src/orangutan-server/src/routes/templates/generate-token.html.tera new file mode 100644 index 0000000..9e83369 --- /dev/null +++ b/src/orangutan-server/src/routes/templates/generate-token.html.tera @@ -0,0 +1,60 @@ +{% extends "base.html" %} + +{% block main %} +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + {% if link %} + {{ link }} + {% endif %} +
+{% endblock main %} + +{% block style %} +{{ super() }} + +.form, .page-content { + display: grid; + gap: 1em; +} + +.form-content { + display: grid; + gap: 0.5em; +} + +.form-field { + display: grid; + gap: 0.25em; +} + +.form input[type=submit] { + font-size: medium; + margin: 0 auto; + min-width: 15%; + max-width: fit-content; +} + +.generated-link { + line-break: anywhere; +} +{% endblock style %} diff --git a/src/orangutan-server/src/util.rs b/src/orangutan-server/src/util/mod.rs similarity index 91% rename from src/orangutan-server/src/util.rs rename to src/orangutan-server/src/util/mod.rs index f50f226..478e7e2 100644 --- a/src/orangutan-server/src/util.rs +++ b/src/orangutan-server/src/util/mod.rs @@ -1,3 +1,8 @@ +#[cfg(feature = "templating")] +pub mod templating; +#[cfg(feature = "token-generator")] +mod website_root; + use biscuit_auth::{ builder::{Fact, Term}, Biscuit, @@ -7,6 +12,8 @@ use rocket::http::{Cookie, CookieJar, SameSite}; use time::Duration; use tracing::error; +#[cfg(feature = "token-generator")] +pub use self::website_root::WebsiteRoot; use crate::{ config::TOKEN_COOKIE_NAME, routes::debug_routes::{ErrorLog, ERRORS}, diff --git a/src/orangutan-server/src/util/templating.rs b/src/orangutan-server/src/util/templating.rs new file mode 100644 index 0000000..c4258d7 --- /dev/null +++ b/src/orangutan-server/src/util/templating.rs @@ -0,0 +1,126 @@ +use rocket::serde::Serialize; +use tera::Context; + +use super::error; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Could not serialize Tera context: {0}")] + ContextError(tera::Error), + #[error("Tera render failed: {0}")] + RenderError(tera::Error), +} + +pub fn render( + tera: &tera::Tera, + template: &str, + context: C, +) -> Result { + let tera_ctx = Context::from_serialize(context).map_err(Error::ContextError)?; + tera.render(template, &tera_ctx).map_err(Error::RenderError) +} + +/// © https://github.com/rwf2/Rocket/blob/4a00c1fe7793c0a1ede33882540cd45be3804ba4/contrib/dyn_templates/src/template.rs#L299-L400 +/// +/// A macro to easily create a template rendering context. +/// +/// Invocations of this macro expand to a value of an anonymous type which +/// implements [`Serialize`]. Fields can be literal expressions or variables +/// captured from a surrounding scope, as long as all fields implement +/// `Serialize`. +/// +/// # Examples +/// +/// The following code: +/// +/// ```rust +/// # #[macro_use] extern crate rocket; +/// # use rocket_dyn_templates::{Template, context}; +/// #[get("/")] +/// fn render_index(foo: u64) -> Template { +/// Template::render("index", context! { +/// // Note that shorthand field syntax is supported. +/// // This is equivalent to `foo: foo,` +/// foo, +/// bar: "Hello world", +/// }) +/// } +/// ``` +/// +/// is equivalent to the following, but without the need to manually define an +/// `IndexContext` struct: +/// +/// ```rust +/// # use rocket_dyn_templates::Template; +/// # use rocket::serde::Serialize; +/// # use rocket::get; +/// #[derive(Serialize)] +/// # #[serde(crate = "rocket::serde")] +/// struct IndexContext<'a> { +/// foo: u64, +/// bar: &'a str, +/// } +/// +/// #[get("/")] +/// fn render_index(foo: u64) -> Template { +/// Template::render("index", IndexContext { +/// foo, +/// bar: "Hello world", +/// }) +/// } +/// ``` +/// +/// ## Nesting +/// +/// Nested objects can be created by nesting calls to `context!`: +/// +/// ```rust +/// # use rocket_dyn_templates::context; +/// # fn main() { +/// let ctx = context! { +/// planet: "Earth", +/// info: context! { +/// mass: 5.97e24, +/// radius: "6371 km", +/// moons: 1, +/// }, +/// }; +/// # } +/// ``` +#[macro_export] +macro_rules! context { + ($($key:ident $(: $value:expr)?),*$(,)?) => {{ + use rocket::serde::ser::{Serialize, Serializer, SerializeMap}; + use ::std::fmt::{Debug, Formatter}; + use ::std::result::Result; + + #[allow(non_camel_case_types)] + struct ContextMacroCtxObject<$($key: Serialize),*> { + $($key: $key),* + } + + #[allow(non_camel_case_types)] + impl<$($key: Serialize),*> Serialize for ContextMacroCtxObject<$($key),*> { + fn serialize(&self, serializer: S) -> Result + where S: Serializer, + { + let mut map = serializer.serialize_map(None)?; + $(map.serialize_entry(stringify!($key), &self.$key)?;)* + map.end() + } + } + + #[allow(non_camel_case_types)] + impl<$($key: Debug + Serialize),*> Debug for ContextMacroCtxObject<$($key),*> { + fn fmt(&self, f: &mut Formatter<'_>) -> ::std::fmt::Result { + f.debug_struct("context!") + $(.field(stringify!($key), &self.$key))* + .finish() + } + } + + ContextMacroCtxObject { + $($key $(: $value)?),* + } + }}; +} diff --git a/src/orangutan-server/src/util/website_root.rs b/src/orangutan-server/src/util/website_root.rs new file mode 100644 index 0000000..a0b41be --- /dev/null +++ b/src/orangutan-server/src/util/website_root.rs @@ -0,0 +1,41 @@ +use std::ops::Deref; + +use lazy_static::lazy_static; +use rocket::{ + request::{FromRequest, Outcome}, + Ignite, Request, Rocket, +}; +use tracing::error; + +lazy_static! { + static ref WEBSITE_ROOT: String = std::env::var("WEBSITE_ROOT").unwrap_or_default(); +} + +pub struct WebsiteRoot(String); + +impl Deref for WebsiteRoot { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for WebsiteRoot { + type Error = &'static str; + + async fn from_request(_req: &'r Request<'_>) -> Outcome { + Outcome::Success(Self(WEBSITE_ROOT.to_owned())) + } +} + +impl rocket::Sentinel for WebsiteRoot { + fn abort(_rocket: &Rocket) -> bool { + if WEBSITE_ROOT.is_empty() { + error!("Environment variable `WEBSITE_ROOT` not found."); + return true; + } + false + } +}