this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

you wouldn't... upload a car

phil c2efc82b 40d217de

+212 -9
+27
Cargo.lock
··· 319 319 "matchit", 320 320 "memchr", 321 321 "mime", 322 + "multer", 322 323 "percent-encoding", 323 324 "pin-project-lite", 324 325 "rustversion", ··· 911 912 "sec1", 912 913 "subtle", 913 914 "zeroize", 915 + ] 916 + 917 + [[package]] 918 + name = "encoding_rs" 919 + version = "0.8.35" 920 + source = "registry+https://github.com/rust-lang/crates.io-index" 921 + checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 922 + dependencies = [ 923 + "cfg-if", 914 924 ] 915 925 916 926 [[package]] ··· 1953 1963 "tagptr", 1954 1964 "thiserror 1.0.69", 1955 1965 "uuid", 1966 + ] 1967 + 1968 + [[package]] 1969 + name = "multer" 1970 + version = "3.1.0" 1971 + source = "registry+https://github.com/rust-lang/crates.io-index" 1972 + checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" 1973 + dependencies = [ 1974 + "bytes", 1975 + "encoding_rs", 1976 + "futures-util", 1977 + "http", 1978 + "httparse", 1979 + "memchr", 1980 + "mime", 1981 + "spin", 1982 + "version_check", 1956 1983 ] 1957 1984 1958 1985 [[package]]
+1 -1
Cargo.toml
··· 4 4 5 5 6 6 [workspace.dependencies] 7 - axum = "0.8.4" 7 + axum = { version = "0.8.4", features = ["multipart"] } 8 8 atrium-common = "0.1.3" 9 9 atrium-crypto = "0.1.3" 10 10 atrium-api = "0.25.7"
+19 -2
shared/challenges_markdown/three/part_two.md
··· 1 - This will be them creating a car export with one collection in it and it holds the verification code. We tell them the 2 - collection, rkey, and code to place in it 1 + 2 + make a valid CAR file and upload it 3 + 4 + the inspection will check for: 5 + 6 + - **Collection:** `codes.advent.challenge.day` 7 + - **Record key:** `3` 8 + - **Field:** `verificationCode` set to `{{code}}` 9 + 10 + any valid DID will do for the commit 11 + 12 + (TODO: we're not checking the signature, right? are we checking CID?) 13 + 14 + <form method="post" action="/day/3/upload-car" enctype="multipart/form-data"> 15 + <div class="flex items-center gap-4"> 16 + <input type="file" name="car_file" accept=".car" class="file-input file-input-bordered w-full max-w-xs" required /> 17 + <button type="submit" class="btn btn-primary">Inspect your CAR</button> 18 + </div> 19 + </form>
+1 -1
shared/src/advent/challenges/day_three/day_three.rs
··· 24 24 } 25 25 26 26 fn has_part_two(&self) -> bool { 27 - false 27 + true 28 28 } 29 29 30 30 fn requires_manual_verification_part_one(&self) -> bool {
+37 -3
shared/src/advent/challenges/day_three/repo.rs
··· 7 7 Repository, 8 8 blockstore::{CarStore, MemoryBlockStore}, 9 9 }; 10 - use serde::Serialize; 10 + use serde::{Deserialize, Serialize}; 11 11 12 12 const CHALLENGE_DID: &str = "did:plc:3oktyyf7u4ecnvdwf3ogehpd"; 13 13 14 14 #[derive(Debug, thiserror::Error)] 15 - pub enum CarBuildError { 15 + pub enum CarFail { 16 16 #[error("Signing error: {0}")] 17 17 Signing(#[from] atrium_crypto::Error), 18 18 #[error("Repo error: {0}")] 19 19 Repo(#[from] atrium_repo::repo::Error), 20 20 #[error("CAR error: {0}")] 21 21 Car(#[from] atrium_repo::blockstore::CarError), 22 + #[error("Bad car: {0}")] 23 + BadCar(String), 22 24 } 23 25 24 26 /// secret record type ··· 30 32 pub message: &'a str, 31 33 } 32 34 35 + /// uploaded record type 36 + #[derive(Debug, Deserialize)] 37 + #[serde(rename_all = "camelCase")] 38 + pub struct SubmittedRecord { 39 + pub verification_code: String, 40 + pub response: Option<String>, 41 + } 42 + 33 43 /// make a single-record atproto car 34 44 /// 35 45 /// returns CAR bytes ··· 37 47 collection: &Nsid, 38 48 rkey: &RecordKey, 39 49 verification_code: &str, 40 - ) -> Result<Vec<u8>, CarBuildError> { 50 + ) -> Result<Vec<u8>, CarFail> { 41 51 let did = CHALLENGE_DID.parse().unwrap(); 42 52 43 53 // set up repo ··· 67 77 repo.export_into(&mut car).await?; 68 78 69 79 Ok(car_bytes) 80 + } 81 + 82 + /// get a verification code (and joke response??? from submitted car) 83 + pub async fn inspect_car( 84 + car_bytes: &[u8], 85 + collection: &Nsid, 86 + rkey: &RecordKey, 87 + ) -> Result<SubmittedRecord, CarFail> { 88 + let mut car = CarStore::open(Cursor::new(car_bytes)).await?; 89 + 90 + let root = car 91 + .roots() 92 + .next() 93 + .ok_or(CarFail::BadCar("no roots".into()))?; 94 + 95 + let mut repo = Repository::open(&mut car, root).await?; 96 + 97 + let key = format!("{}/{}", collection.as_str(), rkey.as_str()); 98 + let record = repo 99 + .get_raw(&key) 100 + .await? 101 + .ok_or(CarFail::BadCar("missing record".into()))?; 102 + 103 + Ok(record) 70 104 } 71 105 72 106 #[cfg(test)]
+120
web/src/handlers/custom/day_three.rs
··· 1 + use crate::{AppState, error_response, session::AxumSessionStore}; 2 + use axum::{ 3 + extract::{Multipart, State}, 4 + http::StatusCode, 5 + response::{IntoResponse, Redirect, Response}, 6 + }; 7 + use crate::session::{FlashMessage, set_flash_message}; 8 + use shared::advent::challenges::day_three::DayThree; 9 + use shared::advent::{AdventChallenge, CompletionStatus}; 10 + use super::log_and_respond; 11 + 12 + pub async fn inspect_car( 13 + state: State<AppState>, 14 + mut session: AxumSessionStore, 15 + mut multipart: Multipart, 16 + ) -> Result<impl IntoResponse, Response> { 17 + let did = session.get_did().ok_or_else(|| { 18 + error_response(StatusCode::FORBIDDEN, "You need to be logged in") 19 + })?; 20 + 21 + // Extract the uploaded file from the multipart form 22 + let mut car_bytes: Option<Vec<u8>> = None; 23 + while let Some(field) = multipart.next_field().await.map_err( 24 + log_and_respond(StatusCode::BAD_REQUEST, "Invalid multipart data") 25 + )? { 26 + if field.name() == Some("car_file") { 27 + car_bytes = Some( 28 + field 29 + .bytes() 30 + .await 31 + .map_err(log_and_respond(StatusCode::BAD_REQUEST, "Failed to read upload"))? 32 + .to_vec() 33 + ); 34 + break; 35 + } 36 + } 37 + 38 + let Some(car_bytes) = car_bytes else { 39 + return Err(error_response(StatusCode::BAD_REQUEST, "No car_file field in upload")) 40 + }; 41 + 42 + // Check that part one is done and part two is in progress 43 + let challenge = DayThree { 44 + pool: state.postgres_pool.clone(), 45 + oauth_client: None, 46 + }; 47 + 48 + let status = challenge 49 + .get_completed_status(Some(did.clone())) 50 + .await 51 + .map_err(log_and_respond(StatusCode::INTERNAL_SERVER_ERROR, "Error checking status"))?; 52 + 53 + if status != CompletionStatus::PartOne { 54 + return Err(error_response(StatusCode::BAD_REQUEST, "Complete part one first")); 55 + } 56 + 57 + // Get the expected verification code for part two 58 + let progress = challenge 59 + .get_days_challenge(&did) 60 + .await 61 + .map_err(log_and_respond(StatusCode::INTERNAL_SERVER_ERROR, "Error loading challenge"))? 62 + .ok_or_else(|| error_response(StatusCode::BAD_REQUEST, "Challenge not started"))?; 63 + 64 + let expected_code = progress.verification_code_two 65 + .ok_or_else(|| error_response(StatusCode::BAD_REQUEST, "Part two not started"))?; 66 + 67 + let res = shared::advent::challenges::day_three::repo::inspect_car( 68 + &car_bytes, 69 + &("codes.advent.challenge.day".parse().unwrap()), 70 + &("3".parse().unwrap()), 71 + ) 72 + .await; 73 + 74 + let record = match res { 75 + Ok(r) => r, 76 + Err(e) => { 77 + log::warn!("bad uploaded car? {e}"); 78 + set_flash_message( 79 + &mut session, 80 + "part_two_result", 81 + FlashMessage::Error( 82 + "Sorry, but your car failed inspection.".into() 83 + ), 84 + ) 85 + .await 86 + .map_err(log_and_respond(StatusCode::INTERNAL_SERVER_ERROR, "Session error"))?; 87 + 88 + return Ok(Redirect::to("/day/3#part_two")); 89 + } 90 + }; 91 + 92 + if !record.verification_code.eq_ignore_ascii_case(&expected_code) { 93 + set_flash_message( 94 + &mut session, 95 + "part_two_result", 96 + FlashMessage::Error("Your CAR looks nice! But not that verification code...".into()), 97 + ) 98 + .await 99 + .map_err(log_and_respond(StatusCode::INTERNAL_SERVER_ERROR, "Session error"))?; 100 + 101 + return Ok(Redirect::to("/day/3#part_two")); 102 + } 103 + 104 + challenge 105 + .complete_part_two(did) 106 + .await 107 + .map_err(log_and_respond(StatusCode::INTERNAL_SERVER_ERROR, "Error completing"))?; 108 + 109 + set_flash_message( 110 + &mut session, 111 + "part_two_result", 112 + FlashMessage::Success("You built a valid CAR file! Part 2 complete.".into()), 113 + ) 114 + .await 115 + .map_err(log_and_respond(StatusCode::INTERNAL_SERVER_ERROR, "Session error"))?; 116 + 117 + // TODO: do something with their joke answer, if submitted 118 + 119 + Ok(Redirect::to("/day/3#part_two")) 120 + }
+1
web/src/handlers/custom/mod.rs
··· 1 + pub mod day_three; 1 2 pub mod day_five; 2 3 pub mod day_six; 3 4
+2 -2
web/src/handlers/day.rs
··· 88 88 } 89 89 } 90 90 91 - pub(super) fn log_and_respond<E: std::fmt::Display>( 91 + pub(super) fn log_and_respond<E: std::fmt::Debug>( 92 92 status: StatusCode, 93 93 context: &'static str, 94 94 ) -> impl FnOnce(E) -> Response { 95 95 move |err| { 96 - log::error!("{context}: {err}"); 96 + log::error!("{context}: {err:?}"); 97 97 error_response(status, context) 98 98 } 99 99 }
+4
web/src/main.rs
··· 287 287 }, 288 288 ) 289 289 .route( 290 + "/day/3/upload-car", 291 + post(handlers::custom::day_three::inspect_car), 292 + ) 293 + .route( 290 294 "/day/5/{user_did}", 291 295 get(handlers::custom::day_five::create_record_handler), 292 296 )