about summary refs log tree commit diff
diff options
context:
space:
mode:
authorVika <vika@fireburn.ru>2024-07-08 23:56:57 +0300
committerVika <vika@fireburn.ru>2024-07-08 23:57:28 +0300
commitb629dc4f7d03c493a4c173d63ef5205b6a24b838 (patch)
tree2cd2af1c68802903825793c443a908685bd6b06e
parent4b7832e7a75ef202c60dc6c00f7518975e64f03a (diff)
downloadkittybox-b629dc4f7d03c493a4c173d63ef5205b6a24b838.tar.zst
WIP: admin (not wired up yet, and DEFINITELY NOT SECURE, DO NOT WIRE UP)
-rw-r--r--src/admin/mod.rs114
-rw-r--r--templates/src/admin.rs5
-rw-r--r--templates/src/lib.rs2
3 files changed, 121 insertions, 0 deletions
diff --git a/src/admin/mod.rs b/src/admin/mod.rs
new file mode 100644
index 0000000..abc4515
--- /dev/null
+++ b/src/admin/mod.rs
@@ -0,0 +1,114 @@
+//! Admin dashboard for Kittybox and its related API routes.
+// This needs to be rewritten to have proper authentication.
+//
+// The cookies will be shared with the login system, and should check
+// if the login is the same as the website owner. If it is, the admin
+// dashboard and API should be available.
+//
+// Alternatively, the API could check for an access token with _some
+// kind_ of privileged scope, like `kittybox:admin` (namespaced to
+// prevent collisions with future well-known scopes).
+use std::collections::HashSet;
+
+use axum::extract::Host;
+use axum::response::{Response, IntoResponse};
+use axum::{Extension, Form};
+use axum_extra::extract::CookieJar;
+use hyper::StatusCode;
+
+use crate::database::settings::{SiteName, Setting};
+use crate::database::{Storage, StorageError};
+use crate::indieauth::backend::AuthBackend;
+
+#[derive(serde::Deserialize)]
+struct NameChange {
+    name: String
+}
+
+#[derive(serde::Deserialize)]
+struct PasswordChange {
+    old_password: String,
+    new_password: String
+}
+
+static SESSION_STORE: std::sync::LazyLock<tokio::sync::RwLock<HashSet<uuid::Uuid>>> = std::sync::LazyLock::new(|| Default::default());
+
+
+async fn set_name<D: Storage + 'static>(
+    Host(host): Host,
+    Extension(db): Extension<D>,
+    Form(NameChange { name }): Form<NameChange>
+) -> Result<(), StorageError> {
+    db.set_setting::<SiteName>(&host, name).await
+}
+
+async fn get_name<D: Storage + 'static>(Host(host): Host, Extension(db): Extension<D>) -> Result<String, StorageError> {
+    db.get_setting::<SiteName>(&host).await.map(|name| name.as_ref().to_owned())
+}
+
+async fn change_password<A: AuthBackend>(
+    Host(host): Host,
+    Extension(auth): Extension<A>,
+    Form(PasswordChange { old_password, new_password }): Form<PasswordChange>
+) -> StatusCode {
+    let website = url::Url::parse(&format!("https://{host}/")).unwrap();
+    if auth.verify_password(&website, old_password).await.is_ok() {
+        if let Err(err) = auth.enroll_password(&website, new_password).await {
+            tracing::error!("Error changing password: {}", err);
+            StatusCode::INTERNAL_SERVER_ERROR
+        } else {
+            StatusCode::OK
+        }
+    } else {
+        StatusCode::BAD_REQUEST
+    }
+}
+
+impl axum::response::IntoResponse for StorageError {
+    fn into_response(self) -> axum::response::Response {
+        let code = match self.kind() {
+            crate::database::ErrorKind::Backend => StatusCode::INTERNAL_SERVER_ERROR,
+            crate::database::ErrorKind::PermissionDenied => StatusCode::FORBIDDEN,
+            crate::database::ErrorKind::JsonParsing => StatusCode::BAD_REQUEST,
+            crate::database::ErrorKind::NotFound => StatusCode::NOT_FOUND,
+            crate::database::ErrorKind::BadRequest => StatusCode::BAD_REQUEST,
+            crate::database::ErrorKind::Conflict => StatusCode::CONFLICT,
+            crate::database::ErrorKind::Other => StatusCode::INTERNAL_SERVER_ERROR,
+        };
+
+        (code, self.to_string()).into_response()
+    }
+}
+
+
+async fn dashboard<D: Storage + 'static, A: AuthBackend>(
+    Host(host): Host,
+    cookies: CookieJar,
+    Extension(db): Extension<D>,
+    Extension(auth): Extension<A>
+) -> axum::response::Response {
+
+    let page = kittybox_frontend_renderer::admin::AdminHome {};
+
+    (page.to_string().into_response())
+}
+
+
+pub fn router<D: Storage + 'static, A: AuthBackend>(db: D, auth: A) -> axum::Router {
+    axum::Router::new()
+        .nest("/.kittybox/admin", axum::Router::new()
+            // routes go here
+            .route(
+                "/",
+                axum::routing::get(dashboard::<D, A>)
+            )
+            .route(
+                "/api/settings/name",
+                axum::routing::post(set_name::<D>)
+                    .get(get_name::<D>)
+            )
+            .route("/api/settings/password", axum::routing::post(change_password::<A>))
+            .layer(Extension(db))
+            .layer(Extension(auth))
+        )
+}
diff --git a/templates/src/admin.rs b/templates/src/admin.rs
new file mode 100644
index 0000000..2837b34
--- /dev/null
+++ b/templates/src/admin.rs
@@ -0,0 +1,5 @@
+markup::define! {
+    AdminHome {
+        p { "This is an example admin homepage." }
+    }
+}
diff --git a/templates/src/lib.rs b/templates/src/lib.rs
index 8d5d5fa..dd263e9 100644
--- a/templates/src/lib.rs
+++ b/templates/src/lib.rs
@@ -9,6 +9,8 @@ pub use login::LoginPage;
 mod mf2;
 pub use mf2::{Entry, VCard, Feed, Food, POSTS_PER_PAGE};
 
+pub mod admin;
+
 pub mod assets {
     use axum::response::{IntoResponse, Response};
     use axum::extract::Path;