···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+
+65-78
shared/src/advent/challenges/day_one.rs
···22use crate::advent::day::Day;
33use crate::advent::{AdventChallenge, AdventError, ChallengeCheckResponse};
44use crate::atrium::safe_check_unknown_record_parse;
55+use crate::lexicons::codes::advent;
56use async_trait::async_trait;
67use atrium_api::types::Collection;
78use sqlx::PgPool;
···3031 did: String,
3132 _verification_code: Option<String>,
3233 ) -> Result<ChallengeCheckResponse, AdventError> {
3333- match &self.oauth_client {
3434- None => Err(AdventError::ShouldNotHappen(
3434+ let client = self
3535+ .oauth_client
3636+ .as_ref()
3737+ .ok_or(AdventError::ShouldNotHappen(
3538 "No oauth client. This should not happen".to_string(),
3636- )),
3737- Some(client) => {
3838- match client
3939- .api
4040- .com
4141- .atproto
4242- .repo
4343- .get_record(
4444- atrium_api::com::atproto::repo::get_record::ParametersData {
4545- cid: None,
4646- collection: crate::lexicons::codes::advent::challenge::Day::NSID
4747- .parse()
4848- .unwrap(),
4949- repo: did.parse().unwrap(),
5050- rkey: "1".parse().unwrap(),
5151- }
5252- .into(),
5353- )
5454- .await
5555- {
5656- Ok(record) => {
5757- //TODO trouble, and make it double
5858- let challenge = self.get_days_challenge(did.clone()).await?;
3939+ ))?;
59406060- match challenge {
6161- None => {
6262- log::error!(
6363- "Could not find a challenge record for day: {} for the user: {}",
6464- self.day(),
6565- did.clone()
6666- );
6767- Err(AdventError::ShouldNotHappen(
6868- "Could not find a challenge record".to_string(),
6969- ))
7070- }
7171- Some(challenge) => {
7272- let parse_record_result =
7373- safe_check_unknown_record_parse::<
7474- crate::lexicons::codes::advent::challenge::day::RecordData,
7575- >(record.value.clone());
4141+ let record_res = client
4242+ .api
4343+ .com
4444+ .atproto
4545+ .repo
4646+ .get_record(
4747+ atrium_api::com::atproto::repo::get_record::ParametersData {
4848+ cid: None,
4949+ collection: advent::challenge::Day::NSID.parse().unwrap(),
5050+ repo: did.parse().unwrap(),
5151+ rkey: "1".parse().unwrap(),
5252+ }
5353+ .into(),
5454+ )
5555+ .await;
5656+5757+ let record = match record_res {
5858+ Ok(r) => r,
5959+ Err(e) => {
6060+ log::error!("Error getting record: {}", e);
6161+ return Ok(ChallengeCheckResponse::Incorrect("Does not appear to be a record in your repo in the collection codes.advent.challenge.day with the record key of 1".to_string()));
6262+ }
6363+ };
6464+6565+ let Some(challenge) = self.get_days_challenge(&did).await? else {
6666+ log::error!("Could not find a challenge record for day: 1 for the user: {did:?}");
6767+ return Err(AdventError::ShouldNotHappen(
6868+ "Could not find challenge record".to_string(),
6969+ ));
7070+ };
76717777- match parse_record_result {
7878- Ok(record_data) => {
7979- match record_data.part_one
8080- == challenge
8181- .verification_code_one
8282- .unwrap_or("".to_string())
8383- {
8484- true => Ok(ChallengeCheckResponse::Correct),
8585- false => {
8686- Ok(ChallengeCheckResponse::Incorrect(format!(
8787- "The code {} is incorrect",
8888- record_data.part_one
8989- )))
9090- }
9191- }
9292- }
9393- Err(err) => {
9494- log::error!("Error parsing record: {}", err);
9595- Ok(ChallengeCheckResponse::Incorrect(format!(
9696- "There is a record at the correct location, but it does not seem like it is correct. Try again:\n{err}"
9797- )))
9898- }
9999- }
100100- }
101101- }
102102- }
103103- Err(err) => {
104104- log::error!("Error getting record: {}", err);
105105- Ok(ChallengeCheckResponse::Incorrect("Does not appear to be a record in your repo in the collection codes.advent.challenge.day with the record key of 1".to_string()))
106106- }
107107- }
7272+ let record_data = match safe_check_unknown_record_parse::<advent::challenge::day::RecordData>(
7373+ record.value.clone(),
7474+ ) {
7575+ Ok(rd) => rd,
7676+ Err(e) => {
7777+ log::error!("Error parsing record: {e}");
7878+ return Ok(ChallengeCheckResponse::Incorrect(format!(
7979+ "There is a record at the correct location, but it does not seem like it is correct. Try again:\n{e}"
8080+ )));
10881 }
109109- }
8282+ };
8383+8484+ let verification_code =
8585+ challenge
8686+ .verification_code_one
8787+ .ok_or(AdventError::ShouldNotHappen(
8888+ "no verification code for day 1 challenge :/".to_string(),
8989+ ))?;
9090+9191+ Ok(if record_data.part_one == verification_code {
9292+ ChallengeCheckResponse::Correct
9393+ } else {
9494+ ChallengeCheckResponse::Incorrect(format!(
9595+ "The code {} is incorrect",
9696+ record_data.part_one
9797+ ))
9898+ })
11099 }
111100112101 ///TODO this is just a straight copy and paste of part one since it's a proof of concept needs to share code better between the two
···128117 .get_record(
129118 atrium_api::com::atproto::repo::get_record::ParametersData {
130119 cid: None,
131131- collection: crate::lexicons::codes::advent::challenge::Day::NSID
132132- .parse()
133133- .unwrap(),
120120+ collection: advent::challenge::Day::NSID.parse().unwrap(),
134121 repo: did.parse().unwrap(),
135122 rkey: "1".parse().unwrap(),
136123 }
···140127 {
141128 Ok(record) => {
142129 //TODO trouble, and make it double
143143- let challenge = self.get_days_challenge(did.clone()).await?;
130130+ let challenge = self.get_days_challenge(&did).await?;
144131145132 match challenge {
146133 None => {
···156143 Some(challenge) => {
157144 let parse_record_result =
158145 safe_check_unknown_record_parse::<
159159- crate::lexicons::codes::advent::challenge::day::RecordData,
146146+ advent::challenge::day::RecordData,
160147 >(record.value.clone());
161148162149 match parse_record_result {
···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,
···186199187200 async fn get_days_challenge(
188201 &self,
189189- did: String,
202202+ did: &str,
190203 ) -> Result<Option<ChallengeProgress>, AdventError> {
191191- let day = self.day();
192204 Ok(sqlx::query_as::<_, ChallengeProgress>(
193205 "SELECT * FROM challenges WHERE user_did = $1 AND day = $2",
194206 )
195207 .bind(did)
196196- .bind(day as i16)
208208+ .bind(self.day() as i16)
197209 .fetch_optional(self.pool())
198210 .await?)
199211 }
···325337 },
326338 }
327339}
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+}
···1616pub mod models;
1717pub mod web_helpers;
18181919+#[rustfmt::skip]
1920pub mod lexicons;
20212122/// OAuthClientType to make it easier to access the OAuthClient in web requests
···11-pub mod day;
21pub mod auth;
22+pub mod day;
+41-9
web/src/main.rs
···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···52525353 pub fn get_did(&self) -> Option<String> {
5454 self.data.did.clone()
5555+ }
5656+5757+ /// 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
5561 }
56625763 ///Gets the message as well as removes it from the session
+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}