about summary refs log tree commit diff
path: root/src/admin
diff options
context:
space:
mode:
Diffstat (limited to 'src/admin')
-rw-r--r--src/admin/mod.rs114
1 files changed, 114 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))
+        )
+}