diff --git a/.env.template b/.env.template index 5be6212..9e36594 100644 --- a/.env.template +++ b/.env.template @@ -9,8 +9,10 @@ PROD=true # Set's the hostname for oauth #OAUTH_HOST=advent.codes -# Enable global day unlock via the settings table (for workshop use) +# Enable global day unlock via the settings table #GLOBAL_UNLOCK_ENABLED=true +# Comma separated list of DIDs allowed to access the /admin page +#ADMIN_DIDS=did:plc:example1,did:plc:example2 # Challenge account. The account that writes some records for challenges CHALLENGE_PDS=https://skeetcentral.com diff --git a/web/src/handlers/admin.rs b/web/src/handlers/admin.rs new file mode 100644 index 0000000..5c236f8 --- /dev/null +++ b/web/src/handlers/admin.rs @@ -0,0 +1,93 @@ +use axum::extract::State; +use axum::response::{IntoResponse, Redirect}; +use axum::Form; +use axum::http::StatusCode; +use shared::advent::get_global_unlock_day; +use sqlx::PgPool; + +use crate::session::AxumSessionStore; +use crate::templates::HtmlTemplate; +use crate::templates::admin::AdminTemplate; + +fn get_admin_allow_list() -> Vec { + std::env::var("ADMIN_DIDS") + .unwrap_or_default() + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect() +} + +fn is_admin(did: Option<&String>) -> bool { + let did = match did { + Some(d) => d, + None => return false, + }; + let allow_list = get_admin_allow_list(); + allow_list.contains(did) +} + +pub async fn admin_page_handler( + State(pool): State, + session: AxumSessionStore, +) -> impl IntoResponse { + let did = session.get_did(); + + if !is_admin(did.as_ref()) { + return (StatusCode::FORBIDDEN, "You are not authorized to access this page.").into_response(); + } + + let current_day = get_global_unlock_day(&pool).await.unwrap_or(1); + + HtmlTemplate(AdminTemplate { + title: "Admin - Global Unlock", + current_unlock_day: current_day, + is_logged_in: session.logged_in(), + message: None, + }) + .into_response() +} + +#[derive(Debug, serde::Deserialize)] +pub struct AdminForm { + pub action: String, +} + +pub async fn admin_post_handler( + State(pool): State, + session: AxumSessionStore, + Form(form): Form, +) -> impl IntoResponse { + let did = session.get_did(); + + if !is_admin(did.as_ref()) { + return (StatusCode::FORBIDDEN, "You are not authorized to access this page.").into_response(); + } + + let current_day = get_global_unlock_day(&pool).await.unwrap_or(1); + + let new_day: i32 = match form.action.as_str() { + "up" => (current_day as i32 + 1).min(25), + "down" => (current_day as i32 - 1).max(1), + _ => current_day as i32, + }; + + let result = sqlx::query("UPDATE settings SET unlocked_up_to_day = $1") + .bind(new_day) + .execute(&pool) + .await; + + match result { + Ok(_) => Redirect::to("/admin").into_response(), + Err(e) => { + log::error!("Failed to update global unlock day: {}", e); + HtmlTemplate(AdminTemplate { + title: "Admin - Global Unlock", + current_unlock_day: current_day, + is_logged_in: session.logged_in(), + message: Some("Failed to update the unlock day.".to_string()), + }) + .into_response() + } + } +} diff --git a/web/src/handlers/mod.rs b/web/src/handlers/mod.rs index d833fac..28d66bd 100644 --- a/web/src/handlers/mod.rs +++ b/web/src/handlers/mod.rs @@ -1,3 +1,4 @@ +pub mod admin; pub mod auth; pub mod custom; pub mod day; diff --git a/web/src/main.rs b/web/src/main.rs index 454ddf0..cfeb50a 100644 --- a/web/src/main.rs +++ b/web/src/main.rs @@ -302,6 +302,8 @@ async fn main() -> Result<(), Box> { "/leaderboard", get(handlers::leaderboard::leaderboard_handler), ) + .route("/admin", get(handlers::admin::admin_page_handler)) + .route("/admin", post(handlers::admin::admin_post_handler)) .route("/login", get(handlers::auth::login_page_handler)) .route("/logout", get(handlers::auth::logout_handler)) .route("/redirect/login", get(handlers::auth::login_handle)) diff --git a/web/src/templates/admin.rs b/web/src/templates/admin.rs new file mode 100644 index 0000000..5861cdf --- /dev/null +++ b/web/src/templates/admin.rs @@ -0,0 +1,10 @@ +use askama::Template; + +#[derive(Template)] +#[template(path = "admin.askama.html")] +pub struct AdminTemplate<'a> { + pub title: &'a str, + pub current_unlock_day: u8, + pub is_logged_in: bool, + pub message: Option, +} diff --git a/web/src/templates/mod.rs b/web/src/templates/mod.rs index 616d6fd..cb14764 100644 --- a/web/src/templates/mod.rs +++ b/web/src/templates/mod.rs @@ -2,6 +2,7 @@ use askama::Template; use axum::http::StatusCode; use axum::response::{Html, IntoResponse, Response}; +pub mod admin; pub mod day; pub mod error; pub mod home; diff --git a/web/templates/admin.askama.html b/web/templates/admin.askama.html new file mode 100644 index 0000000..2741157 --- /dev/null +++ b/web/templates/admin.askama.html @@ -0,0 +1,35 @@ +{% extends "layout.askama.html" %} + +{% block content %} +
+
+
+

Global Unlock

+ + {% if let Some(msg) = message %} +
+ {{ msg }} +
+ {% endif %} + +

Currently unlocked up to day:

+

{{ current_unlock_day }}

+ +
+
+ + +
+
+ + +
+
+
+
+
+{% endblock %}