about summary refs log tree commit diff
path: root/kittybox-rs/src
diff options
context:
space:
mode:
Diffstat (limited to 'kittybox-rs/src')
-rw-r--r--kittybox-rs/src/frontend/onboarding.rs7
-rw-r--r--kittybox-rs/src/index.html182
-rw-r--r--kittybox-rs/src/lib.rs71
-rw-r--r--kittybox-rs/src/main.rs129
-rw-r--r--kittybox-rs/src/media/mod.rs7
-rw-r--r--kittybox-rs/src/micropub/mod.rs13
6 files changed, 165 insertions, 244 deletions
diff --git a/kittybox-rs/src/frontend/onboarding.rs b/kittybox-rs/src/frontend/onboarding.rs
index 08a05ee..e9eceb2 100644
--- a/kittybox-rs/src/frontend/onboarding.rs
+++ b/kittybox-rs/src/frontend/onboarding.rs
@@ -145,3 +145,10 @@ pub async fn post<D: Storage + 'static>(
         }
     }
 }
+
+pub fn router<S: Storage + 'static>(database: S, http: reqwest::Client) -> axum::routing::MethodRouter {
+    axum::routing::get(get)
+        .post(post::<S>)
+        .layer(axum::Extension(database))
+        .layer(axum::Extension(http))
+}
diff --git a/kittybox-rs/src/index.html b/kittybox-rs/src/index.html
deleted file mode 100644
index 1fc2a96..0000000
--- a/kittybox-rs/src/index.html
+++ /dev/null
@@ -1,182 +0,0 @@
-<html>
-    <head>
-        <meta charset="utf-8">
-        <title>Kittybox-Micropub debug client</title>
-        <style type="text/css">
-        * {
-            box-sizing: border-box;
-        }
-        :root {
-            font-family: sans-serif;
-        }
-        body {
-            margin: 0;
-        }
-        body > main {
-            margin: auto;
-            max-width: 1024px;
-        }
-        h1.header {
-            margin-top: 0.75em;
-            text-align: center;
-        }
-        fieldset + fieldset, fieldset + input, section + section, section + fieldset {
-            margin-top: 0.75em;
-        }
-        input[type="submit"] {
-            margin-left: auto;
-            display: block;
-        }
-        form > fieldset > section > label {
-            width: 100%;
-            display: block;
-        }
-        form > fieldset > section > input, form > fieldset > section > textarea {
-            width: 100%;
-        }
-        textarea {
-            min-height: 10em;
-        }
-        </style>
-        <script type="module">
-            const form = document.getElementById("micropub");
-            const channel_select_radio = document.getElementById("select_channels");
-            channel_select_radio.onclick = async () => {
-                const channels = await query_channels()
-                if (channels !== undefined) {
-                    populate_channel_list(channels)
-                }
-            }
-            const no_channel_radio = document.getElementById("no_channel");
-            no_channel_radio.onclick = () => {
-                document.getElementById("channels").style.display = "none";
-                const channel_list = document.getElementById("channels_target")
-                channel_list.innerHTML = "";
-            }
-            function construct_body(form) {
-                return {
-                    type: ["h-entry"],
-                    properties: {
-                        content: [form.elements.content.value],
-                        name: form.elements.name.value ? [form.elements.name.value] : undefined,
-                        category: form.elements.category.value ? form.elements.category.value.split(",").map(val => val.trim()) : undefined,
-                        channel: form.elements.channel_select.value ?  Array.from(form.elements.channel).map(i => i.checked ? i.value : false).filter(i => i) : undefined
-                    }
-                }
-            }
-
-            async function query_channels() {
-                const response = await fetch(form.action + "?q=config", {
-                    headers: {
-                        "Authorization": `Bearer ${form.elements.access_token.value}`
-                    }
-                })
-
-                const config = await response.json();
-
-                return config["channels"]
-            }
-
-            function populate_channel_list(channels) {
-                document.getElementById("channels").style.display = "block";
-                const channel_list = document.getElementById("channels_target")
-                channel_list.innerHTML = "";
-                channels.forEach((channel) => {
-                    const template = document.getElementById("channel_selector").content.cloneNode(true)
-                    const input = template.querySelector("input")
-                    const label = template.querySelector("label")
-                    input.id = `channel_selector_option_${channel.uid}`
-                    input.value = channel.uid
-                    label.for = input.id
-                    label.innerHTML = `<a href="${channel.uid}">${channel.name}</a>`
-
-                    channel_list.appendChild(template)
-                })
-            }
-
-            form.onsubmit = async (event) => {
-                event.preventDefault()
-                console.log(JSON.stringify(construct_body(form)))
-                try {
-                    const response = await fetch(form.action, {
-                        method: form.method,
-                        headers: {
-                            "Authorization": `Bearer ${form.elements.access_token.value}`,
-                            "Content-Type": "application/json"
-                        },
-                        body: JSON.stringify(construct_body(form))
-                    })
-                    if (response.status != 201 || response.status != 202) {
-                        console.error(await response.json());
-                    }
-                    if (response.headers.get("Location")) {
-                        window.location.href = response.headers.get("Location");
-                    }
-                } catch (e) {
-                    console.error(e)
-                }
-            }
-        </script>
-    </head>
-    <body>
-        <h1 class="header">Kittybox-Micropub debug client</h1>
-
-        <main>
-            <p>
-                In a pinch? Lost your Micropub client, but need to make a quick announcement?
-                Worry not, the debug client has your back. <i>I just hope you have a spare Micropub token stored somewhere like I do...</i>
-            </p>
-
-            <form action="/micropub" method="POST" id="micropub">
-                <fieldset>
-                    <legend>Authorization details</legend>
-                    <section>
-                        <label for="access_token">Access token:</label>
-                        <input id="access_token" name="access_token" type="password">
-
-                        <p><a href="https://gimme-a-token.5eb.nl/" target="_blank">Get an access token (will open in a new tab)</a></p>
-                    </section>
-                </fieldset>
-                <fieldset>
-                    <legend>Post details:</legend>
-                    <section>
-                        <label for="name">Name (leave blank for an unnamed post):</label>
-                        <input id="name" type="text">
-                    </section>
-                    <section>
-                        <label for="content">Content:</label>
-                        <textarea id="content" placeholder="Your post's text goes here"></textarea>
-                    </section>
-                    <section>
-                        <label for="category">Categories (separeted by commas):</label>
-                        <input id="category" type="text">
-                    </section>
-                    <fieldset>
-                        <legend>Channels</legend>
-                        <section>
-                            <input type="radio" id="no_channel" name="channel_select" checked value="">
-                            <label for="no_channel">Default channel only</label>
-                        </section>
-
-                        <section>
-                            <input type="radio" id="select_channels" name="channel_select" value="on">
-                            <label for="select_channels">Select channels manually</label>
-                        </section>
-                        
-                        <fieldset id="channels" style="display: none">
-                            <legend>Available channels:</legend>
-                            <template id="channel_selector">
-                                <section>
-                                    <input type="checkbox" name="channel" id="" value="">
-                                    <label for=""></label>
-                                </section>
-                            </template>
-                            <div id="channels_target"></div>
-                        </fieldset>
-                    </fieldset>
-                </fieldset>
-                <input type="submit">
-            </form>
-        </main>
-    </body>
-</html>
\ No newline at end of file
diff --git a/kittybox-rs/src/lib.rs b/kittybox-rs/src/lib.rs
index c1683d9..3f25689 100644
--- a/kittybox-rs/src/lib.rs
+++ b/kittybox-rs/src/lib.rs
@@ -7,5 +7,74 @@ pub mod frontend;
 pub mod tokenauth;
 pub mod media;
 pub mod micropub;
+pub mod indieauth;
 
-pub static MICROPUB_CLIENT: &[u8] = include_bytes!("./index.html");
+pub mod companion {
+    use axum::{
+        extract::{Extension, Path},
+        response::IntoResponse
+    };
+
+    #[derive(Debug, Clone, Copy)]
+    struct Resource {
+        data: &'static [u8],
+        mime: &'static str
+    }
+
+    type ResourceTable = std::sync::Arc<std::collections::HashMap<&'static str, Resource>>;
+
+    #[tracing::instrument]
+    async fn map_to_static(
+        Path(name): Path<String>,
+        Extension(resources): Extension<ResourceTable>
+    ) -> impl IntoResponse {
+        tracing::debug!("Searching for {} in the resource table...", name);
+        if let Some(res) = resources.get(name.as_str()) {
+            (axum::http::StatusCode::OK,
+             [("Content-Type", res.mime)],
+             res.data)
+        } else {
+            #[cfg(debug_assertions)]
+            tracing::error!("Not found");
+            (axum::http::StatusCode::NOT_FOUND,
+             [("Content-Type", "text/plain")],
+             "Not found. Sorry.".as_bytes())
+        }
+    }
+
+    pub fn router() -> axum::Router {
+        let resources = {
+            let mut map = std::collections::HashMap::new();
+
+            macro_rules! register_resource {
+                ($prefix:literal, ($filename:literal, $mime:literal)) => {{
+                    map.insert($filename, Resource {
+                        data: include_bytes!(concat!($prefix, $filename)),
+                        mime: $mime
+                    })
+                }};
+                ($prefix:literal, ($filename:literal, $mime:literal), $( ($f:literal, $m:literal) ),+) => {{
+                    register_resource!($prefix, ($filename, $mime));
+                    register_resource!($prefix, $(($f, $m)),+);
+                }};
+            }
+
+            register_resource! {
+                "../companion-lite/",
+                ("index.html", "text/html; charset=\"utf-8\""),
+                ("main.js", "text/javascript"),
+                ("micropub_api.js", "text/javascript"),
+                ("style.css", "text/css")
+            };
+
+            std::sync::Arc::new(map)
+        };
+
+        axum::Router::new()
+            .route(
+                "/:filename",
+                axum::routing::get(map_to_static)
+                    .layer(Extension(resources))
+            )
+    }
+}
diff --git a/kittybox-rs/src/main.rs b/kittybox-rs/src/main.rs
index 50c0ca5..59c3e69 100644
--- a/kittybox-rs/src/main.rs
+++ b/kittybox-rs/src/main.rs
@@ -110,11 +110,55 @@ async fn main() {
             kittybox::media::storage::file::FileStore::new(path)
         };
 
-        let svc = axum::Router::new()
+        // This code proves that different components of Kittybox can
+        // be split up without hurting the app
+        //
+        // If needed, some features could be omitted from the binary
+        // or just not spun up in the future
+        //
+        // For example, the frontend code could run spearately from
+        // Micropub and only have read access to the database folder
+        let frontend = axum::Router::new()
             .route(
                 "/",
-                axum::routing::get(kittybox::frontend::homepage::<FileStorage>),
-            )
+                axum::routing::get(kittybox::frontend::homepage::<FileStorage>)
+                    .layer(axum::Extension(database.clone())))
+            .route("/.kittybox/static/:path", axum::routing::get(kittybox::frontend::statics))
+            .fallback(
+                axum::routing::get(kittybox::frontend::catchall::<FileStorage>)
+                    .layer(axum::Extension(database.clone())));
+
+        // Onboarding is a bit of a special case. One might argue that
+        // the onboarding makes Kittybox a monolith. This is wrong.
+        // The "onboarding receiver" doesn't need any code from the
+        // onboarding form - they're grouped in a single module for
+        // convenience only, since modifying one usually requires
+        // updating the other to match.
+        //
+        // For example, this "router" just groups two separate methods
+        // in one request, because logically they live in the same
+        // subtree. But one could manually construct only one but not
+        // the other, to receive a "frontend-only" application. Of
+        // course, in this scenario, one must employ a reverse proxy
+        // to distinguish between GET and POST requests to the same
+        // path, and route them to the correct set of endpoints with
+        // write access.
+        let onboarding = axum::Router::new()
+            .route("/.kittybox/onboarding", kittybox::frontend::onboarding::router(
+                database.clone(), http.clone()
+            ));
+
+        let micropub = axum::Router::new()
+            .route("/.kittybox/micropub", kittybox::micropub::router(database.clone(), http.clone()))
+            .nest("/.kittybox/micropub/client", kittybox::companion::router());
+
+        let media = axum::Router::new()
+            .nest("/.kittybox/media", kittybox::media::router(blobstore).layer(axum::Extension(http)));
+
+        /*let indieauth = axum::Router::new()
+            .nest("/.kittybox/indieauth", kittybox::indieauth::router());*/
+
+        let technical = axum::Router::new()
             .route(
                 "/.kittybox/coffee",
                 axum::routing::get(|| async {
@@ -127,71 +171,34 @@ async fn main() {
                 }),
             )
             .route(
-                "/.kittybox/onboarding",
-                axum::routing::get(kittybox::frontend::onboarding::get)
-                    .post(kittybox::frontend::onboarding::post::<FileStorage>),
-            )
-            .route(
-                "/.kittybox/micropub",
-                axum::routing::get(kittybox::micropub::query::<FileStorage>)
-                    .post(kittybox::micropub::post::<FileStorage>)
-                    .layer(
-                        tower_http::cors::CorsLayer::new()
-                            .allow_methods([axum::http::Method::GET, axum::http::Method::POST])
-                            .allow_origin(tower_http::cors::Any),
-                    ),
-            )
-            .route(
-                "/.kittybox/micropub/client",
-                axum::routing::get(|| {
-                    std::future::ready(axum::response::Html(kittybox::MICROPUB_CLIENT))
-                }),
-            )
-            .route(
                 "/.kittybox/health",
-                axum::routing::get(|| async {
+                axum::routing::get(
+                    |axum::Extension(db): axum::Extension<FileStorage>| async move {
                     // TODO health-check the database
                     "OK"
-                }),
+                    }
+                )
+                    .layer(axum::Extension(database))
             )
             .route(
                 "/.kittybox/metrics",
                 axum::routing::get(|| async { todo!() }),
-            )
-            .nest(
-                "/.kittybox/media",
-                axum::Router::new()
-                    .route(
-                        "/",
-                        axum::routing::get(|| async { todo!() })
-                            .post(
-                                kittybox::media::upload::<kittybox::media::FileStore>
-                            ),
-                    )
-                    .route("/uploads/*file", axum::routing::get(
-                        kittybox::media::serve::<kittybox::media::FileStore>
-                    )),
-            )
-            .route(
-                "/.kittybox/static/:path",
-                axum::routing::get(kittybox::frontend::statics),
-            )
-            .fallback(axum::routing::get(
-                kittybox::frontend::catchall::<FileStorage>,
-            ))
-            .layer(axum::Extension(database))
-            .layer(axum::Extension(http))
-            .layer(axum::Extension(kittybox::tokenauth::TokenEndpoint(
-                token_endpoint,
-            )))
-            .layer(axum::Extension(blobstore))
-            .layer(
-                tower::ServiceBuilder::new()
-                    .layer(tower_http::trace::TraceLayer::new_for_http())
-                    .into_inner(),
             );
 
-        // A little dance to turn a potential file descriptor into a guaranteed async network socket
+        let svc = axum::Router::new()
+            .merge(frontend)
+            .merge(onboarding)
+            .merge(micropub)
+            .merge(media)
+            //.merge(indieauth)
+            .merge(technical)
+            .layer(axum::Extension(kittybox::tokenauth::TokenEndpoint(token_endpoint)))
+            .layer(tower::ServiceBuilder::new()
+                   .layer(tower_http::trace::TraceLayer::new_for_http())
+                   .into_inner());
+
+        // A little dance to turn a potential file descriptor into
+        // a guaranteed async network socket
         let tcp_listener: std::net::TcpListener = {
             let mut listenfd = listenfd::ListenFd::from_env();
 
@@ -200,8 +207,8 @@ async fn main() {
             } else {
                 std::net::TcpListener::bind(listen_at).unwrap()
             };
-            // Set the socket to non-blocking so tokio can work with it properly
-            // This is the async magic
+            // Set the socket to non-blocking so tokio can poll it
+            // properly -- this is the async magic!
             tcp_listener.set_nonblocking(true).unwrap();
 
             tcp_listener
diff --git a/kittybox-rs/src/media/mod.rs b/kittybox-rs/src/media/mod.rs
index 4a253d0..b64929d 100644
--- a/kittybox-rs/src/media/mod.rs
+++ b/kittybox-rs/src/media/mod.rs
@@ -98,3 +98,10 @@ pub async fn serve<S: MediaStore>(
         }
     }
 }
+
+pub fn router<S: MediaStore>(blobstore: S) -> axum::Router {
+    axum::Router::new()
+        .route("/", axum::routing::post(upload::<S>))
+        .route("/uploads/*file", axum::routing::get(serve::<S>))
+        .layer(axum::Extension(blobstore))
+}
diff --git a/kittybox-rs/src/micropub/mod.rs b/kittybox-rs/src/micropub/mod.rs
index 3328597..d0aeae0 100644
--- a/kittybox-rs/src/micropub/mod.rs
+++ b/kittybox-rs/src/micropub/mod.rs
@@ -596,6 +596,19 @@ pub async fn query<D: Storage>(
     }
 }
 
+pub fn router<S: Storage + 'static>(storage: S, http: reqwest::Client) -> axum::routing::MethodRouter {
+    axum::routing::get(query::<S>)
+        .post(post::<S>)
+        .layer(tower_http::cors::CorsLayer::new()
+               .allow_methods([
+                   axum::http::Method::GET,
+                   axum::http::Method::POST,
+               ])
+               .allow_origin(tower_http::cors::Any))
+        .layer(axum::Extension(storage))
+        .layer(axum::Extension(http))
+}
+
 #[cfg(test)]
 #[allow(dead_code)]
 impl MicropubQuery {