···99# Set's the hostname for oauth
1010#OAUTH_HOST=advent.codes
11111212-# Enable global day unlock via the settings table (for workshop use)
1212+# Enable global day unlock via the settings table
1313#GLOBAL_UNLOCK_ENABLED=true
1414+# Comma separated list of DIDs allowed to access the /admin page
1515+#ADMIN_DIDS=did:plc:example1,did:plc:example2
14161517# Challenge account. The account that writes some records for challenges
1618CHALLENGE_PDS=https://skeetcentral.com
+93
web/src/handlers/admin.rs
···11+use axum::extract::State;
22+use axum::response::{IntoResponse, Redirect};
33+use axum::Form;
44+use axum::http::StatusCode;
55+use shared::advent::get_global_unlock_day;
66+use sqlx::PgPool;
77+88+use crate::session::AxumSessionStore;
99+use crate::templates::HtmlTemplate;
1010+use crate::templates::admin::AdminTemplate;
1111+1212+fn get_admin_allow_list() -> Vec<String> {
1313+ std::env::var("ADMIN_DIDS")
1414+ .unwrap_or_default()
1515+ .split(',')
1616+ .map(|s| s.trim().to_string())
1717+ .filter(|s| !s.is_empty())
1818+ .collect()
1919+}
2020+2121+fn is_admin(did: Option<&String>) -> bool {
2222+ let did = match did {
2323+ Some(d) => d,
2424+ None => return false,
2525+ };
2626+ let allow_list = get_admin_allow_list();
2727+ allow_list.contains(did)
2828+}
2929+3030+pub async fn admin_page_handler(
3131+ State(pool): State<PgPool>,
3232+ session: AxumSessionStore,
3333+) -> impl IntoResponse {
3434+ let did = session.get_did();
3535+3636+ if !is_admin(did.as_ref()) {
3737+ return (StatusCode::FORBIDDEN, "You are not authorized to access this page.").into_response();
3838+ }
3939+4040+ let current_day = get_global_unlock_day(&pool).await.unwrap_or(1);
4141+4242+ HtmlTemplate(AdminTemplate {
4343+ title: "Admin - Global Unlock",
4444+ current_unlock_day: current_day,
4545+ is_logged_in: session.logged_in(),
4646+ message: None,
4747+ })
4848+ .into_response()
4949+}
5050+5151+#[derive(Debug, serde::Deserialize)]
5252+pub struct AdminForm {
5353+ pub action: String,
5454+}
5555+5656+pub async fn admin_post_handler(
5757+ State(pool): State<PgPool>,
5858+ session: AxumSessionStore,
5959+ Form(form): Form<AdminForm>,
6060+) -> impl IntoResponse {
6161+ let did = session.get_did();
6262+6363+ if !is_admin(did.as_ref()) {
6464+ return (StatusCode::FORBIDDEN, "You are not authorized to access this page.").into_response();
6565+ }
6666+6767+ let current_day = get_global_unlock_day(&pool).await.unwrap_or(1);
6868+6969+ let new_day: i32 = match form.action.as_str() {
7070+ "up" => (current_day as i32 + 1).min(25),
7171+ "down" => (current_day as i32 - 1).max(1),
7272+ _ => current_day as i32,
7373+ };
7474+7575+ let result = sqlx::query("UPDATE settings SET unlocked_up_to_day = $1")
7676+ .bind(new_day)
7777+ .execute(&pool)
7878+ .await;
7979+8080+ match result {
8181+ Ok(_) => Redirect::to("/admin").into_response(),
8282+ Err(e) => {
8383+ log::error!("Failed to update global unlock day: {}", e);
8484+ HtmlTemplate(AdminTemplate {
8585+ title: "Admin - Global Unlock",
8686+ current_unlock_day: current_day,
8787+ is_logged_in: session.logged_in(),
8888+ message: Some("Failed to update the unlock day.".to_string()),
8989+ })
9090+ .into_response()
9191+ }
9292+ }
9393+}
+1
web/src/handlers/mod.rs
···11+pub mod admin;
12pub mod auth;
23pub mod custom;
34pub mod day;
···22use axum::http::StatusCode;
33use axum::response::{Html, IntoResponse, Response};
4455+pub mod admin;
56pub mod day;
67pub mod error;
78pub mod home;