diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/admin/mod.rs | 114 |
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)) + ) +} |