about summary refs log tree commit diff
diff options
context:
space:
mode:
authorVika <vika@fireburn.ru>2021-05-17 04:12:48 +0300
committerVika <vika@fireburn.ru>2021-05-17 04:12:48 +0300
commit696ae495aca701c3431710e5dfc03e15aba2f74e (patch)
tree3b58d7cb23a8edd5fdb7121ab420ed60a9af64cb
parent3dbe61f57873881dfbf5da8a335762a0e1dccbb5 (diff)
downloadkittybox-696ae495aca701c3431710e5dfc03e15aba2f74e.tar.zst
Refactoring, easter egg, healthcheck endpoint, support for rel= indieweb APIs and preparation for onboarding
-rw-r--r--src/frontend/mod.rs45
-rw-r--r--src/frontend/style.css7
-rw-r--r--src/lib.rs15
-rw-r--r--src/main.rs22
-rw-r--r--src/micropub/mod.rs7
-rw-r--r--src/micropub/post.rs7
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<String>,
+    microsub: Option<String>
+}
+
 #[derive(Deserialize)]
 struct QueryParams {
     after: Option<String>
@@ -437,9 +450,16 @@ async fn get_post_from_database<S: Storage>(db: &S, url: &str, after: Option<Str
     }
 }
 
+pub async fn coffee<S: Storage>(_: Request<ApplicationState<S>>) -> 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<S: Storage>(req: Request<ApplicationState<S>>) -> Result {
     let backend = &req.state().storage;
     let query = req.query::<QueryParams>()?;
+    let authorization_endpoint = req.state().authorization_endpoint.to_string();
+    let token_endpoint = req.state().token_endpoint.to_string();
     let user: Option<String> = None;
 
     #[cfg(any(not(debug_assertions), test))]
@@ -470,6 +490,10 @@ pub async fn mainpage<S: Storage>(req: Request<ApplicationState<S>>) -> 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<S: Storage>(req: Request<ApplicationState<S>>) -> Result {
 
 pub async fn render_post<S: Storage>(req: Request<ApplicationState<S>>) -> Result {
     let query = req.query::<QueryParams>()?;
+    let authorization_endpoint = req.state().authorization_endpoint.to_string();
+    let token_endpoint = req.state().token_endpoint.to_string();
     let user: Option<String> = None;
 
     #[cfg(any(not(debug_assertions), test))]
@@ -501,6 +527,10 @@ pub async fn render_post<S: Storage>(req: Request<ApplicationState<S>>) -> 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<S> tide::Middleware<ApplicationState<S>> for ErrorHandlerMiddleware where
     S: crate::database::Storage
 {
     async fn handle(&self, request: Request<ApplicationState<S>>, next: Next<'_, ApplicationState<S>>) -> 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<StatusCode> = None;
         if let Some(err) = res.downcast_error::<FrontendError>() {
@@ -527,7 +559,14 @@ impl<S> tide::Middleware<ApplicationState<S>> 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<StorageBackend>
@@ -14,6 +13,7 @@ where
     StorageBackend: database::Storage + Send + Sync + 'static
 {
     token_endpoint: surf::Url,
+    authorization_endpoint: surf::Url,
     media_endpoint: Option<String>,
     http_client: surf::Client,
     storage: StorageBackend
@@ -27,21 +27,27 @@ fn equip_app<Storage>(mut app: App<Storage>) -> App<Storage>
 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<String>) -> App<database::RedisStorage> {
+pub async fn get_app_with_redis(token_endpoint: surf::Url, authorization_endpoint: surf::Url, redis_uri: String, media_endpoint: Option<String>) -> App<database::RedisStorage> {
     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<String> = 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<FixedOffset>;
@@ -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<S: Storage>(req: Request<ApplicationState<S>>, body: serde_json::Value) -> Result {
+pub async fn new_post<S: Storage>(req: Request<ApplicationState<S>>, body: serde_json::Value) -> Result {
     // First, check for rights.
     let user = req.ext::<User>().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<S: Storage>(req: Request<ApplicationState<S>>, 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))