diff --git a/src/bin/server.rs b/src/bin/server.rs index 3f375f5..31d32d5 100644 --- a/src/bin/server.rs +++ b/src/bin/server.rs @@ -28,7 +28,7 @@ use zettle_db::{ }, html::{ account::{delete_account, set_api_key, show_account_page, update_timezone}, - admin_panel::show_admin_panel, + admin_panel::{show_admin_panel, toggle_registration_status}, documentation::index::show_documentation_index, gdpr::{accept_gdpr, deny_gdpr}, index::index_handler, @@ -172,6 +172,7 @@ fn html_routes( .route("/queue/:delivery_tag", delete(delete_task)) .route("/account", get(show_account_page)) .route("/admin", get(show_admin_panel)) + .route("/toggle-registrations", patch(toggle_registration_status)) .route("/set-api-key", post(set_api_key)) .route("/update-timezone", patch(update_timezone)) .route("/delete-account", delete(delete_account)) diff --git a/src/server/routes/html/admin_panel.rs b/src/server/routes/html/admin_panel.rs index a9c3c10..a79b3af 100644 --- a/src/server/routes/html/admin_panel.rs +++ b/src/server/routes/html/admin_panel.rs @@ -1,6 +1,7 @@ use axum::{ extract::State, response::{IntoResponse, Redirect}, + Form, }; use axum_session_auth::AuthSession; use axum_session_surreal::SessionSurrealPool; @@ -13,6 +14,8 @@ use crate::{ storage::types::{analytics::Analytics, system_settings::SystemSettings, user::User}, }; +use super::render_block; + page_data!(AdminPanelData, "auth/admin_panel.html", { user: User, settings: SystemSettings, @@ -24,7 +27,7 @@ pub async fn show_admin_panel( State(state): State, auth: AuthSession, Surreal>, ) -> Result { - // Early return if the user is not authenticated + // Early return if the user is not authenticated and admin let user = match auth.current_user { Some(user) if user.admin => user, _ => return Ok(Redirect::to("/").into_response()), @@ -55,3 +58,61 @@ pub async fn show_admin_panel( Ok(output.into_response()) } + +fn checkbox_to_bool<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + match String::deserialize(deserializer) { + Ok(string) => Ok(string == "on"), + Err(_) => Ok(false), + } +} + +#[derive(Deserialize)] +pub struct RegistrationToggleInput { + #[serde(default)] + #[serde(deserialize_with = "checkbox_to_bool")] + registration_open: bool, +} + +#[derive(Serialize)] +pub struct RegistrationToggleData { + settings: SystemSettings, +} + +pub async fn toggle_registration_status( + State(state): State, + auth: AuthSession, Surreal>, + Form(input): Form, +) -> Result { + // Early return if the user is not authenticated and admin + let _user = match auth.current_user { + Some(user) if user.admin => user, + _ => return Ok(Redirect::to("/").into_response()), + }; + + let current_settings = SystemSettings::get_current(&state.surreal_db_client) + .await + .map_err(|e| HtmlError::new(e, state.templates.clone()))?; + + let new_settings = SystemSettings { + registrations_enabled: input.registration_open, + ..current_settings.clone() + }; + + SystemSettings::update(&state.surreal_db_client, new_settings.clone()) + .await + .map_err(|e| HtmlError::new(e, state.templates.clone()))?; + + let output = render_block( + AdminPanelData::template_name(), + "registration_status_input", + RegistrationToggleData { + settings: new_settings, + }, + state.templates.clone(), + )?; + + Ok(output.into_response()) +} diff --git a/src/storage/types/system_settings.rs b/src/storage/types/system_settings.rs index 724a05f..7a4801a 100644 --- a/src/storage/types/system_settings.rs +++ b/src/storage/types/system_settings.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use crate::{error::AppError, storage::db::SurrealDbClient}; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct SystemSettings { #[serde(deserialize_with = "deserialize_flexible_id")] pub id: String, diff --git a/src/storage/types/user.rs b/src/storage/types/user.rs index f384f3e..b7ff3c1 100644 --- a/src/storage/types/user.rs +++ b/src/storage/types/user.rs @@ -41,6 +41,19 @@ impl Authentication> for User { } } +fn validate_timezone(input: &str) -> String { + use chrono_tz::Tz; + + // Check if it's a valid IANA timezone identifier + match input.parse::() { + Ok(_) => input.to_owned(), + Err(_) => { + tracing::warn!("Invalid timezone '{}' received, defaulting to UTC", input); + "UTC".to_owned() + } + } +} + impl User { pub async fn create_new( email: String, @@ -54,6 +67,7 @@ impl User { return Err(AppError::Auth("Registration is not allowed".into())); } + let validated_tz = validate_timezone(&timezone); let now = Utc::now(); let id = Uuid::new_v4().to_string(); @@ -76,7 +90,7 @@ impl User { .bind(("password", password)) .bind(("created_at", now)) .bind(("updated_at", now)) - .bind(("timezone", timezone)) + .bind(("timezone", validated_tz)) .await? .take(1)?; diff --git a/templates/auth/admin_panel.html b/templates/auth/admin_panel.html index cce7b72..b930470 100644 --- a/templates/auth/admin_panel.html +++ b/templates/auth/admin_panel.html @@ -29,8 +29,12 @@
Registration
diff --git a/todo.md b/todo.md index 8dd6c30..4a98c24 100644 --- a/todo.md +++ b/todo.md @@ -1,12 +1,12 @@ \[\] admin controls re registration \[\] archive ingressed webpage \[\] configs primarily get envs -\[\] html ingression \[\] view content \[\] view graph map \[\] view latest \[x\] add user_id to ingress objects \[x\] gdpr +\[x\] html ingression \[x\] hx-redirect \[x\] ios shortcut generation \[x\] job queue