//! 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>> = std::sync::LazyLock::new(|| Default::default()); async fn set_name( Host(host): Host, Extension(db): Extension, Form(NameChange { name }): Form ) -> Result<(), StorageError> { db.set_setting::(&host, name).await } async fn get_name(Host(host): Host, Extension(db): Extension) -> Result { db.get_setting::(&host).await.map(|name| name.as_ref().to_owned()) } async fn change_password( Host(host): Host, Extension(auth): Extension, Form(PasswordChange { old_password, new_password }): Form ) -> 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( Host(host): Host, cookies: CookieJar, Extension(db): Extension, Extension(auth): Extension ) -> axum::response::Response { let page = kittybox_frontend_renderer::admin::AdminHome {}; (page.to_string().into_response()) } pub fn router(db: D, auth: A) -> axum::Router { axum::Router::new() .nest("/.kittybox/admin", axum::Router::new() // routes go here .route( "/", axum::routing::get(dashboard::) ) .route( "/api/settings/name", axum::routing::post(set_name::) .get(get_name::) ) .route("/api/settings/password", axum::routing::post(change_password::)) .layer(Extension(db)) .layer(Extension(auth)) ) }