···11+Great job beating Part 1! Now onto Part 2.
22+33+Keeping it simple proof of concept, blah, blah will have a real one here another time. Add a new field `partTwo` to the
44+record with the value `{{code}}`
55+
···5252 Two,
5353}
54545555+#[derive(Clone, Copy, Debug)]
5556pub enum CompletionStatus {
5657 ///None of the day's challenges have been completed
5758 None,
···118119 // }
119120 // }
120121122122+ /// Does part one require manual verification code input from the user?
123123+ /// Default is false (verification happens automatically via backend)
124124+ fn requires_manual_verification_part_one(&self) -> bool {
125125+ false
126126+ }
127127+128128+ /// Does part two require manual verification code input from the user?
129129+ /// Default is false (verification happens automatically via backend)
130130+ fn requires_manual_verification_part_two(&self) -> bool {
131131+ false
132132+ }
133133+121134 /// The text Markdown for challenge 1
122135 fn markdown_text_part_one(
123136 &self,
···324337 },
325338 }
326339}
340340+341341+/// Get completion status for all 25 days at once
342342+/// Returns a Vec of tuples (day_number, CompletionStatus) for days 1-25
343343+pub async fn get_all_days_completion_status(
344344+ pool: &PgPool,
345345+ did: Option<&String>,
346346+) -> Result<Vec<(u8, CompletionStatus)>, AdventError> {
347347+ // If no user is logged in, return None for all days
348348+ let did = match did {
349349+ Some(d) => d,
350350+ None => {
351351+ return Ok((1..=25).map(|day| (day, CompletionStatus::None)).collect());
352352+ }
353353+ };
354354+355355+ // Query all challenge progress for this user in a single query
356356+ let results = sqlx::query!(
357357+ "SELECT day, time_challenge_one_completed, time_challenge_two_completed
358358+ FROM challenges
359359+ WHERE user_did = $1 AND day BETWEEN 1 AND 25
360360+ ORDER BY day",
361361+ did
362362+ )
363363+ .fetch_all(pool)
364364+ .await?;
365365+366366+ // Create a map of day -> completion status
367367+ let mut status_map: std::collections::HashMap<u8, CompletionStatus> =
368368+ std::collections::HashMap::new();
369369+370370+ for row in results {
371371+ let day = row.day as u8;
372372+ let status = match (
373373+ row.time_challenge_one_completed,
374374+ row.time_challenge_two_completed,
375375+ ) {
376376+ (None, None) => CompletionStatus::None,
377377+ (Some(_), None) => CompletionStatus::PartOne,
378378+ (Some(_), Some(_)) => CompletionStatus::Both,
379379+ _ => CompletionStatus::None,
380380+ };
381381+ status_map.insert(day, status);
382382+ }
383383+384384+ // Build the result vec for all 25 days
385385+ let result: Vec<(u8, CompletionStatus)> = (1..=25)
386386+ .map(|day| {
387387+ let status = status_map
388388+ .get(&day)
389389+ .copied()
390390+ .unwrap_or(CompletionStatus::None);
391391+ (day, status)
392392+ })
393393+ .collect();
394394+395395+ Ok(result)
396396+}
···11use crate::{
22- templates::HtmlTemplate, templates::error::ErrorTemplate, templates::home::HomeTemplate,
22+ session::AxumSessionStore,
33+ templates::HtmlTemplate,
44+ templates::error::ErrorTemplate,
55+ templates::home::{DayStatus, HomeTemplate},
36};
47use atrium_identity::{
58 did::{CommonDidResolver, CommonDidResolverConfig, DEFAULT_PLC_DIRECTORY_URL},
···1114};
1215use axum::{
1316 Router,
1717+ extract::State,
1418 http::StatusCode,
1519 middleware,
1620 response::IntoResponse,
···2125use chrono::Datelike;
2226use dotenv::dotenv;
2327use redis::AsyncCommands;
2828+use rust_embed::RustEmbed;
2429use shared::{
2525- HandleResolver, OAuthClientType, atrium::dns_resolver::HickoryDnsTxtResolver,
2626- atrium::stores::AtriumSessionStore, atrium::stores::AtriumStateStore,
3030+ HandleResolver, OAuthClientType,
3131+ advent::{CompletionStatus, get_all_days_completion_status},
3232+ atrium::dns_resolver::HickoryDnsTxtResolver,
3333+ atrium::stores::AtriumSessionStore,
3434+ atrium::stores::AtriumStateStore,
2735};
2836use sqlx::{PgPool, postgres::PgPoolOptions};
2937use std::{
···4048mod handlers;
41494250extern crate dotenv;
4343-5151+//
4452mod extractors;
4553mod redis_session_store;
4654mod session;
4755mod templates;
4856mod unlock;
49575858+#[derive(RustEmbed, Clone)]
5959+#[folder = "./public"]
6060+struct Assets;
6161+5062#[derive(Clone)]
5163struct AppState {
5264 postgres_pool: PgPool,
···5971fn oauth_scopes() -> Vec<Scope> {
6072 vec![
6173 Scope::Known(KnownScope::Atproto),
7474+ // Scope::Known(KnownScope::TransitionGeneric),
7575+ //This looks like it HAS to have the full collection name, before i want to say it worked with wildcard
6276 //Gives full CRUD to the codes.advent.* collection
6363- Scope::Unknown("repo:codes.advent.*".to_string()),
7777+ Scope::Unknown("repo:codes.advent.test".to_string()),
6478 ]
6579}
6680···7084 HtmlTemplate(ErrorTemplate {
7185 title: "at://advent - Error",
7286 message,
8787+ is_logged_in: false,
7388 }),
7489 ))
7590}
···174189 .map(|val| val == "true")
175190 .unwrap_or_else(|_| true);
176191 log::info!("listening on http://{}", addr);
192192+177193 let app = Router::new()
178194 .route("/", get(home_handler))
179195 .route(
···193209 },
194210 )
195211 .route("/login", get(handlers::auth::login_page_handler))
196196- .route("/handle", get(handlers::auth::handle_root_handler))
197197- .route("/login/{handle}", get(handlers::auth::login_handle))
212212+ .route("/logout", get(handlers::auth::logout_handler))
213213+ .route("/redirect/login", get(handlers::auth::login_handle))
198214 .route(
199215 "/oauth/callback",
200216 get(handlers::auth::oauth_callback_handler),
201217 )
218218+ .nest_service("/public", axum_embed::ServeEmbed::<Assets>::new())
202219 .layer(session_layer)
203220 .with_state(app_state)
204221 .layer(TraceLayer::new_for_http());
···207224}
208225209226/// Landing page showing currently unlocked days and a login button
210210-async fn home_handler() -> impl IntoResponse {
227227+async fn home_handler(State(pool): State<PgPool>, session: AxumSessionStore) -> impl IntoResponse {
211228 //TODO make a helper function for this since it is similar to the middleware
212229 let now = chrono::Utc::now();
213230 let mut unlocked: Vec<u8> = Vec::new();
···229246 }
230247 }
231248249249+ // Get completion status for all days at once
250250+ let did = session.get_did();
251251+ let is_logged_in = session.logged_in();
252252+ let all_statuses = get_all_days_completion_status(&pool, did.as_ref())
253253+ .await
254254+ .unwrap_or_else(|_| (1..=25).map(|day| (day, CompletionStatus::None)).collect());
255255+256256+ // Filter to only include unlocked days
257257+ let unlocked_with_status: Vec<DayStatus> = all_statuses
258258+ .into_iter()
259259+ .filter(|(day, _)| unlocked.contains(day))
260260+ .map(|(day, status)| DayStatus { day, status })
261261+ .collect();
262262+232263 HtmlTemplate(HomeTemplate {
233264 title: "at://advent",
234234- unlocked_days: unlocked,
265265+ unlocked_days: unlocked_with_status,
266266+ is_logged_in,
235267 })
236268}
+7-1
web/src/session.rs
···4141impl AxumSessionStore {
4242 const SESSION_DATA_KEY: &'static str = "session.data";
43434444- pub fn _logged_in(&self) -> bool {
4444+ pub fn logged_in(&self) -> bool {
4545 self.data.did.is_some()
4646 }
4747···5454 self.data.did.clone()
5555 }
56565757+ /// Clears the session data (logs out the user)
5858+ pub async fn clear_session(&mut self) -> Result<(), tower_sessions::session::Error> {
5959+ self.data = SessionData::default();
6060+ Self::update_session(&self.session, &self.data).await
6161+ }
6262+5763 ///Gets the message as well as removes it from the session
5864 pub async fn get_flash_message(
5965 &mut self,
+6
web/src/templates/day.rs
···1414 //If these are set than it was a redirect from checking the challenge.
1515 pub part_one_submit_message: Option<FlashMessage>,
1616 pub part_two_submit_message: Option<FlashMessage>,
1717+1818+ // Flags to indicate if manual code input is required
1919+ pub requires_code_input_part_one: bool,
2020+ pub requires_code_input_part_two: bool,
2121+2222+ pub is_logged_in: bool,
1723}