From 696ae495aca701c3431710e5dfc03e15aba2f74e Mon Sep 17 00:00:00 2001 From: Vika Date: Mon, 17 May 2021 04:12:48 +0300 Subject: Refactoring, easter egg, healthcheck endpoint, support for rel= indieweb APIs and preparation for onboarding --- src/frontend/mod.rs | 45 ++++++++++++++++++++++++++++++++++++++++++--- src/frontend/style.css | 7 +++++-- src/lib.rs | 15 +++++++++++---- src/main.rs | 22 ++++++++++++++++++++-- src/micropub/mod.rs | 7 ++++--- src/micropub/post.rs | 7 ++++--- 6 files changed, 86 insertions(+), 17 deletions(-) diff --git a/src/frontend/mod.rs b/src/frontend/mod.rs index aaaa2b2..891e944 100644 --- a/src/frontend/mod.rs +++ b/src/frontend/mod.rs @@ -1,4 +1,4 @@ -use serde::Deserialize; +use serde::{Serialize, Deserialize}; use tide::{Request, Response, Result, StatusCode, Next}; use log::{info,error}; use crate::ApplicationState; @@ -11,6 +11,7 @@ mod templates { use http_types::StatusCode; use ellipse::Ellipse; use chrono; + use super::IndiewebEndpoints; /// Return a pretty location specifier from a geo: URI. fn decode_geo_uri(uri: &str) -> String { @@ -30,7 +31,7 @@ mod templates { } markup::define! { - Template<'a>(title: &'a str, content: String) { + Template<'a>(title: &'a str, endpoints: IndiewebEndpoints, content: String) { @markup::doctype() html { head { @@ -38,6 +39,10 @@ mod templates { link[rel="preconnect", href="https://fonts.gstatic.com"]; link[rel="stylesheet", href="/static/style.css"]; meta[name="viewport", content="initial-scale=1, width=device-width"]; + // TODO: link rel= for common IndieWeb APIs: webmention, microsub + link[rel="micropub", href="/micropub"]; // Static, because it's built into the server itself + link[rel="authorization_endpoint", href=&endpoints.authorization_endpoint]; + link[rel="token_endpoint", href=&endpoints.token_endpoint]; } body { nav#headerbar { @@ -379,6 +384,14 @@ mod templates { use templates::{Template,ErrorPage,MainPage}; +#[derive(Clone, Serialize, Deserialize)] +pub struct IndiewebEndpoints { + authorization_endpoint: String, + token_endpoint: String, + webmention: Option, + microsub: Option +} + #[derive(Deserialize)] struct QueryParams { after: Option @@ -437,9 +450,16 @@ async fn get_post_from_database(db: &S, url: &str, after: Option(_: Request>) -> Result { + Err(FrontendError::with_code(StatusCode::ImATeapot, "Someone asked this website to brew them some coffee..."))?; + return Ok(Response::builder(500).build()) // unreachable +} + pub async fn mainpage(req: Request>) -> Result { let backend = &req.state().storage; let query = req.query::()?; + let authorization_endpoint = req.state().authorization_endpoint.to_string(); + let token_endpoint = req.state().token_endpoint.to_string(); let user: Option = None; #[cfg(any(not(debug_assertions), test))] @@ -470,6 +490,10 @@ pub async fn mainpage(req: Request>) -> Result { .content_type("text/html; charset=utf-8") .body(Template { title: &format!("{} - Main page", url.host().unwrap().to_string()), + endpoints: IndiewebEndpoints { + authorization_endpoint, token_endpoint, + webmention: None, microsub: None + }, content: MainPage { feed: &feed?, card: &card? @@ -481,6 +505,8 @@ pub async fn mainpage(req: Request>) -> Result { pub async fn render_post(req: Request>) -> Result { let query = req.query::()?; + let authorization_endpoint = req.state().authorization_endpoint.to_string(); + let token_endpoint = req.state().token_endpoint.to_string(); let user: Option = None; #[cfg(any(not(debug_assertions), test))] @@ -501,6 +527,10 @@ pub async fn render_post(req: Request>) -> Resul .content_type("text/html; charset=utf-8") .body(Template { title: post["properties"]["name"][0].as_str().unwrap_or(&format!("Note at {}", url.host().unwrap().to_string())), + endpoints: IndiewebEndpoints { + authorization_endpoint, token_endpoint, + webmention: None, microsub: None + }, content: template }.to_string() ).build()) @@ -513,6 +543,8 @@ impl tide::Middleware> for ErrorHandlerMiddleware where S: crate::database::Storage { async fn handle(&self, request: Request>, next: Next<'_, ApplicationState>) -> Result { + let authorization_endpoint = request.state().authorization_endpoint.to_string(); + let token_endpoint = request.state().token_endpoint.to_string(); let mut res = next.run(request).await; let mut code: Option = None; if let Some(err) = res.downcast_error::() { @@ -527,7 +559,14 @@ impl tide::Middleware> for ErrorHandlerMiddleware where if let Some(code) = code { res.set_status(code); res.set_content_type("text/html; charset=utf-8"); - res.set_body(Template { title: "Error", content: ErrorPage { code }.to_string()}.to_string()); + res.set_body(Template { + title: "Error", + endpoints: IndiewebEndpoints { + authorization_endpoint, token_endpoint, + webmention: None, microsub: None + }, + content: ErrorPage { code }.to_string() + }.to_string()); } Ok(res) } diff --git a/src/frontend/style.css b/src/frontend/style.css index 2c43808..1d6586b 100644 --- a/src/frontend/style.css +++ b/src/frontend/style.css @@ -5,6 +5,9 @@ --font-normal: 'Lato', sans-serif; --font-accent: 'Caveat', cursive; --type-scale: 1.250; + + --primary-accent: purple; + --secondary-accent: gold; } * { box-sizing: border-box; @@ -31,9 +34,9 @@ h6, .normal {font-size: 1rem;} small, .small { font-size: 0.8em; } nav#headerbar { - background: purple; + background: var(--primary-accent); color: whitesmoke; - border-bottom: .75rem solid gold; + border-bottom: .75rem solid var(--secondary-accent); padding: .3rem; vertical-align: center; position: sticky; diff --git a/src/lib.rs b/src/lib.rs index 949b9ac..ed30b94 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,6 @@ mod micropub; mod frontend; use crate::indieauth::IndieAuthMiddleware; -use crate::micropub::{get_handler,post_handler}; #[derive(Clone)] pub struct ApplicationState @@ -14,6 +13,7 @@ where StorageBackend: database::Storage + Send + Sync + 'static { token_endpoint: surf::Url, + authorization_endpoint: surf::Url, media_endpoint: Option, http_client: surf::Client, storage: StorageBackend @@ -27,21 +27,27 @@ fn equip_app(mut app: App) -> App where Storage: database::Storage + Send + Sync + Clone { - app.at("/micropub").with(IndieAuthMiddleware::new()).get(get_handler).post(post_handler); + app.at("/micropub").with(IndieAuthMiddleware::new()) + .get(micropub::get_handler) + .post(micropub::post_handler); // The Micropub client. It'll start small, but could grow into something full-featured! app.at("/micropub/client").get(|_: Request<_>| async move { Ok(Response::builder(200).body(MICROPUB_CLIENT).content_type("text/html").build()) }); - app.at("/").with(frontend::ErrorHandlerMiddleware {}).get(frontend::mainpage); + app.at("/").with(frontend::ErrorHandlerMiddleware {}) + .get(frontend::mainpage); app.at("/static/*path").with(frontend::ErrorHandlerMiddleware {}).get(frontend::handle_static); app.at("/*path").with(frontend::ErrorHandlerMiddleware {}).get(frontend::render_post); + app.at("/coffee").with(frontend::ErrorHandlerMiddleware {}).get(frontend::coffee); + app.at("/health").get(|_| async { Ok("OK") }); app } -pub async fn get_app_with_redis(token_endpoint: surf::Url, redis_uri: String, media_endpoint: Option) -> App { +pub async fn get_app_with_redis(token_endpoint: surf::Url, authorization_endpoint: surf::Url, redis_uri: String, media_endpoint: Option) -> App { let app = tide::with_state(ApplicationState { token_endpoint, media_endpoint, + authorization_endpoint, storage: database::RedisStorage::new(redis_uri).await.unwrap(), http_client: surf::Client::new(), }); @@ -55,6 +61,7 @@ pub async fn get_app_with_test_redis(token_endpoint: surf::Url) -> (database::Re let backend = database::RedisStorage::new(redis_instance.uri().to_string()).await.unwrap(); let app = tide::with_state(ApplicationState { token_endpoint, media_endpoint: None, + authorization_endpoint: Url::parse("https://indieauth.com/auth"), storage: backend.clone(), http_client: surf::Client::new(), }); diff --git a/src/main.rs b/src/main.rs index 778aa4a..ce654df 100644 --- a/src/main.rs +++ b/src/main.rs @@ -39,9 +39,27 @@ async fn main() -> Result<(), std::io::Error> { std::process::exit(1) } } + let authorization_endpoint: Url; + match env::var("AUTHORIZATION_ENDPOINT") { + Ok(val) => { + debug!("Auth endpoint: {}", val); + match Url::parse(&val) { + Ok(val) => authorization_endpoint = val, + _ => { + error!("Authorization endpoint URL cannot be parsed, aborting."); + std::process::exit(1) + } + } + }, + Err(_) => { + error!("AUTHORIZATION_ENDPOINT is not set, will not be able to confirm token and ID requests using IndieAuth!"); + std::process::exit(1) + } + } + let media_endpoint: Option = env::var("MEDIA_ENDPOINT").ok(); let host = env::var("SERVE_AT").ok().unwrap_or_else(|| "0.0.0.0:8080".to_string()); - let app = micropub::get_app_with_redis(token_endpoint, redis_uri, media_endpoint).await; + let app = micropub::get_app_with_redis(token_endpoint, authorization_endpoint, redis_uri, media_endpoint).await; app.listen(host).await -} \ No newline at end of file +} diff --git a/src/micropub/mod.rs b/src/micropub/mod.rs index ec5cd87..9bc553c 100644 --- a/src/micropub/mod.rs +++ b/src/micropub/mod.rs @@ -1,5 +1,6 @@ -mod get; -mod post; +pub mod get; +pub mod post; pub use get::get_handler; -pub use post::post_handler; \ No newline at end of file +pub use post::post_handler; +pub use post::normalize_mf2; \ No newline at end of file diff --git a/src/micropub/post.rs b/src/micropub/post.rs index c1efd61..6183906 100644 --- a/src/micropub/post.rs +++ b/src/micropub/post.rs @@ -47,7 +47,7 @@ fn get_folder_from_type(post_type: &str) -> String { }).to_string() } -fn normalize_mf2(mut body: serde_json::Value, user: &User) -> (String, serde_json::Value) { +pub fn normalize_mf2(mut body: serde_json::Value, user: &User) -> (String, serde_json::Value) { // Normalize the MF2 object here. let me = &user.me; let published: DateTime; @@ -150,9 +150,10 @@ fn normalize_mf2(mut body: serde_json::Value, user: &User) -> (String, serde_jso return (body["properties"]["uid"][0].as_str().unwrap().to_string(), body) } -async fn new_post(req: Request>, body: serde_json::Value) -> Result { +pub async fn new_post(req: Request>, body: serde_json::Value) -> Result { // First, check for rights. let user = req.ext::().unwrap(); + let storage = &req.state().storage; if !user.check_scope("create") { return error_json!(401, "invalid_scope", "Not enough privileges to post. Try a token with a \"create\" scope instead.") } @@ -169,7 +170,7 @@ async fn new_post(req: Request>, body: serde_jso return error_json!(403, "forbidden", "You're trying to post to someone else's website...") } - let storage = &req.state().storage; + match storage.post_exists(&uid).await { Ok(exists) => if exists { return error_json!(409, "already_exists", format!("A post with the exact same UID already exists in the database: {}", uid)) -- cgit 1.4.1