From 8d29145bb2a88e7894117b76e72d1493d6343fb2 Mon Sep 17 00:00:00 2001 From: Bailey Townsend Date: Fri, 27 Mar 2026 10:17:28 -0700 Subject: [PATCH] global lock --- .env.template | 3 + migrations/20260327000000_create_settings.sql | 8 +++ shared/src/advent/mod.rs | 10 +++ web/src/main.rs | 64 +++++++++++-------- web/src/unlock.rs | 20 +++++- 5 files changed, 79 insertions(+), 26 deletions(-) create mode 100644 migrations/20260327000000_create_settings.sql diff --git a/.env.template b/.env.template index 9952294..5be6212 100644 --- a/.env.template +++ b/.env.template @@ -9,6 +9,9 @@ PROD=true # Set's the hostname for oauth #OAUTH_HOST=advent.codes +# Enable global day unlock via the settings table (for workshop use) +#GLOBAL_UNLOCK_ENABLED=true + # Challenge account. The account that writes some records for challenges CHALLENGE_PDS=https://skeetcentral.com CHALLENGE_IDENTITY= diff --git a/migrations/20260327000000_create_settings.sql b/migrations/20260327000000_create_settings.sql new file mode 100644 index 0000000..8a5ea67 --- /dev/null +++ b/migrations/20260327000000_create_settings.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS settings ( + id BIGSERIAL PRIMARY KEY, + unlocked_up_to_day INT NOT NULL DEFAULT 1, + CONSTRAINT settings_day_range CHECK (unlocked_up_to_day >= 1 AND unlocked_up_to_day <= 25) +); + +-- Insert a single default row +INSERT INTO settings (unlocked_up_to_day) VALUES (1); diff --git a/shared/src/advent/mod.rs b/shared/src/advent/mod.rs index c4f203c..b22b553 100644 --- a/shared/src/advent/mod.rs +++ b/shared/src/advent/mod.rs @@ -389,6 +389,16 @@ fn get_markdown_options() -> Options { } } +/// Get the globally unlocked day from the settings table. +/// Returns the day number that all users are unlocked up to. +pub async fn get_global_unlock_day(pool: &PgPool) -> Result { + let result = sqlx::query_scalar::<_, i32>("SELECT unlocked_up_to_day FROM settings LIMIT 1") + .fetch_optional(pool) + .await?; + + Ok(result.unwrap_or(1) as u8) +} + /// Get completion status for all 25 days at once /// Returns a Vec of tuples (day_number, CompletionStatus) for days 1-25 pub async fn get_all_days_completion_status( diff --git a/web/src/main.rs b/web/src/main.rs index a27c7d1..454ddf0 100644 --- a/web/src/main.rs +++ b/web/src/main.rs @@ -31,7 +31,7 @@ use redis::AsyncCommands; use rust_embed::RustEmbed; use shared::{ HandleResolver, OAuthClientType, PasswordAgent, - advent::{CompletionStatus, get_all_days_completion_status}, + advent::{CompletionStatus, get_all_days_completion_status, get_global_unlock_day}, atrium::dns_resolver::HickoryDnsTxtResolver, atrium::stores::AtriumSessionStore, atrium::stores::AtriumStateStore, @@ -341,34 +341,48 @@ async fn home_handler(State(pool): State, session: AxumSessionStore) -> .await .unwrap_or_else(|_| (1..=25).map(|day| (day, CompletionStatus::None)).collect()); - //HACK Yeah I don't like it either - bt - let prod: bool = env::var("PROD") - .map(|val| val == "true") - .unwrap_or_else(|_| true); - if prod { + let global_unlock_enabled = env::var("GLOBAL_UNLOCK_ENABLED") + .map(|v| v == "true") + .unwrap_or(false); + + if global_unlock_enabled { + let global_unlock_day = get_global_unlock_day(&pool).await.unwrap_or(1); let implemented_days = shared::advent::get_implemented_days(); - if let Some(&first) = implemented_days.first() { - unlocked.push(first); - } - for window in implemented_days.windows(2) { - let prev = window[0]; - let prev_status = all_statuses - .iter() - .find(|(d, _)| *d == prev) - .map(|(_, s)| s) - .unwrap_or(&CompletionStatus::None); - if *prev_status == CompletionStatus::Both { - unlocked.push(window[1]); - } else if (prev == 4 || prev == 5) && *prev_status == CompletionStatus::PartOne { - //HACK hardcoded for the workshop since we don't have a part 2 for day 4 - unlocked.push(window[1]); - } else { - break; + for d in implemented_days { + if d <= global_unlock_day { + unlocked.push(d); } } } else { - for d in 1..=25 { - unlocked.push(d as u8); + //HACK Yeah I don't like it either - bt + let prod: bool = env::var("PROD") + .map(|val| val == "true") + .unwrap_or_else(|_| true); + if prod { + let implemented_days = shared::advent::get_implemented_days(); + if let Some(&first) = implemented_days.first() { + unlocked.push(first); + } + for window in implemented_days.windows(2) { + let prev = window[0]; + let prev_status = all_statuses + .iter() + .find(|(d, _)| *d == prev) + .map(|(_, s)| s) + .unwrap_or(&CompletionStatus::None); + if *prev_status == CompletionStatus::Both { + unlocked.push(window[1]); + } else if (prev == 4 || prev == 5) && *prev_status == CompletionStatus::PartOne { + //HACK hardcoded for the workshop since we don't have a part 2 for day 4 + unlocked.push(window[1]); + } else { + break; + } + } + } else { + for d in 1..=25 { + unlocked.push(d as u8); + } } } diff --git a/web/src/unlock.rs b/web/src/unlock.rs index 572414f..006b2f5 100644 --- a/web/src/unlock.rs +++ b/web/src/unlock.rs @@ -5,7 +5,7 @@ use axum::{ middleware, response::{self, IntoResponse}, }; -use shared::advent::{CompletionStatus, get_all_days_completion_status}; +use shared::advent::{CompletionStatus, get_all_days_completion_status, get_global_unlock_day}; use sqlx::PgPool; pub async fn unlock( @@ -47,6 +47,24 @@ pub async fn unlock( .into_response(); } + // Check if the day is globally unlocked via the settings table + let global_unlock_enabled = std::env::var("GLOBAL_UNLOCK_ENABLED") + .map(|v| v == "true") + .unwrap_or(false); + + if global_unlock_enabled { + let global_unlock_day = get_global_unlock_day(&pool).await.unwrap_or(1); + if day <= global_unlock_day { + return next.run(request).await; + } else { + return ( + http::StatusCode::FORBIDDEN, + "Now just hold on a minute. It ain't time yet.", + ) + .into_response(); + } + } + // First implemented day is always accessible let pos = implemented_days.iter().position(|&d| d == day).unwrap(); if pos > 0 { -- 2.51.2