···99# Set's the hostname for oauth
1010#OAUTH_HOST=advent.codes
11111212+# Enable global day unlock via the settings table (for workshop use)
1313+#GLOBAL_UNLOCK_ENABLED=true
1414+1215# Challenge account. The account that writes some records for challenges
1316CHALLENGE_PDS=https://skeetcentral.com
1417CHALLENGE_IDENTITY=
+8
migrations/20260327000000_create_settings.sql
···11+CREATE TABLE IF NOT EXISTS settings (
22+ id BIGSERIAL PRIMARY KEY,
33+ unlocked_up_to_day INT NOT NULL DEFAULT 1,
44+ CONSTRAINT settings_day_range CHECK (unlocked_up_to_day >= 1 AND unlocked_up_to_day <= 25)
55+);
66+77+-- Insert a single default row
88+INSERT INTO settings (unlocked_up_to_day) VALUES (1);
+10
shared/src/advent/mod.rs
···389389 }
390390}
391391392392+/// Get the globally unlocked day from the settings table.
393393+/// Returns the day number that all users are unlocked up to.
394394+pub async fn get_global_unlock_day(pool: &PgPool) -> Result<u8, AdventError> {
395395+ let result = sqlx::query_scalar::<_, i32>("SELECT unlocked_up_to_day FROM settings LIMIT 1")
396396+ .fetch_optional(pool)
397397+ .await?;
398398+399399+ Ok(result.unwrap_or(1) as u8)
400400+}
401401+392402/// Get completion status for all 25 days at once
393403/// Returns a Vec of tuples (day_number, CompletionStatus) for days 1-25
394404pub async fn get_all_days_completion_status(
+39-25
web/src/main.rs
···3131use rust_embed::RustEmbed;
3232use shared::{
3333 HandleResolver, OAuthClientType, PasswordAgent,
3434- advent::{CompletionStatus, get_all_days_completion_status},
3434+ advent::{CompletionStatus, get_all_days_completion_status, get_global_unlock_day},
3535 atrium::dns_resolver::HickoryDnsTxtResolver,
3636 atrium::stores::AtriumSessionStore,
3737 atrium::stores::AtriumStateStore,
···341341 .await
342342 .unwrap_or_else(|_| (1..=25).map(|day| (day, CompletionStatus::None)).collect());
343343344344- //HACK Yeah I don't like it either - bt
345345- let prod: bool = env::var("PROD")
346346- .map(|val| val == "true")
347347- .unwrap_or_else(|_| true);
348348- if prod {
344344+ let global_unlock_enabled = env::var("GLOBAL_UNLOCK_ENABLED")
345345+ .map(|v| v == "true")
346346+ .unwrap_or(false);
347347+348348+ if global_unlock_enabled {
349349+ let global_unlock_day = get_global_unlock_day(&pool).await.unwrap_or(1);
349350 let implemented_days = shared::advent::get_implemented_days();
350350- if let Some(&first) = implemented_days.first() {
351351- unlocked.push(first);
352352- }
353353- for window in implemented_days.windows(2) {
354354- let prev = window[0];
355355- let prev_status = all_statuses
356356- .iter()
357357- .find(|(d, _)| *d == prev)
358358- .map(|(_, s)| s)
359359- .unwrap_or(&CompletionStatus::None);
360360- if *prev_status == CompletionStatus::Both {
361361- unlocked.push(window[1]);
362362- } else if (prev == 4 || prev == 5) && *prev_status == CompletionStatus::PartOne {
363363- //HACK hardcoded for the workshop since we don't have a part 2 for day 4
364364- unlocked.push(window[1]);
365365- } else {
366366- break;
351351+ for d in implemented_days {
352352+ if d <= global_unlock_day {
353353+ unlocked.push(d);
367354 }
368355 }
369356 } else {
370370- for d in 1..=25 {
371371- unlocked.push(d as u8);
357357+ //HACK Yeah I don't like it either - bt
358358+ let prod: bool = env::var("PROD")
359359+ .map(|val| val == "true")
360360+ .unwrap_or_else(|_| true);
361361+ if prod {
362362+ let implemented_days = shared::advent::get_implemented_days();
363363+ if let Some(&first) = implemented_days.first() {
364364+ unlocked.push(first);
365365+ }
366366+ for window in implemented_days.windows(2) {
367367+ let prev = window[0];
368368+ let prev_status = all_statuses
369369+ .iter()
370370+ .find(|(d, _)| *d == prev)
371371+ .map(|(_, s)| s)
372372+ .unwrap_or(&CompletionStatus::None);
373373+ if *prev_status == CompletionStatus::Both {
374374+ unlocked.push(window[1]);
375375+ } else if (prev == 4 || prev == 5) && *prev_status == CompletionStatus::PartOne {
376376+ //HACK hardcoded for the workshop since we don't have a part 2 for day 4
377377+ unlocked.push(window[1]);
378378+ } else {
379379+ break;
380380+ }
381381+ }
382382+ } else {
383383+ for d in 1..=25 {
384384+ unlocked.push(d as u8);
385385+ }
372386 }
373387 }
374388
+19-1
web/src/unlock.rs
···55 middleware,
66 response::{self, IntoResponse},
77};
88-use shared::advent::{CompletionStatus, get_all_days_completion_status};
88+use shared::advent::{CompletionStatus, get_all_days_completion_status, get_global_unlock_day};
99use sqlx::PgPool;
10101111pub async fn unlock(
···4545 "This day hasn't been created yet!",
4646 )
4747 .into_response();
4848+ }
4949+5050+ // Check if the day is globally unlocked via the settings table
5151+ let global_unlock_enabled = std::env::var("GLOBAL_UNLOCK_ENABLED")
5252+ .map(|v| v == "true")
5353+ .unwrap_or(false);
5454+5555+ if global_unlock_enabled {
5656+ let global_unlock_day = get_global_unlock_day(&pool).await.unwrap_or(1);
5757+ if day <= global_unlock_day {
5858+ return next.run(request).await;
5959+ } else {
6060+ return (
6161+ http::StatusCode::FORBIDDEN,
6262+ "Now just hold on a minute. It ain't time yet.",
6363+ )
6464+ .into_response();
6565+ }
4866 }
49675068 // First implemented day is always accessible