this repo has no description
0
fork

Configure Feed

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

custom module for day-specific handlers

phil 40d217de 99ac4675

+234 -205
+51
web/src/handlers/custom/day_five.rs
··· 1 + use crate::{ 2 + AppState, 3 + error_response, 4 + session::AxumSessionStore, 5 + }; 6 + use axum::{ 7 + extract::{Path, State}, 8 + http::StatusCode, 9 + response::{IntoResponse, Redirect, Response}, 10 + }; 11 + use shared::advent::challenges::day_five::DayFive; 12 + use super::log_and_respond; 13 + 14 + /// Endpoint for day 5: creates a record on the secret agent's repo with the user's verification code, 15 + /// then redirects back to /day/5 so the user can find the code via firehose/jetstream. 16 + pub async fn create_record_handler( 17 + Path(user_did): Path<String>, 18 + state: State<AppState>, 19 + session: AxumSessionStore, 20 + ) -> Result<impl IntoResponse, Response> { 21 + // Verify the user is logged in and the DID matches 22 + let session_did = session.get_did().ok_or_else(|| { 23 + error_response( 24 + StatusCode::FORBIDDEN, 25 + "You need to be logged in to do this", 26 + ) 27 + })?; 28 + 29 + if session_did != user_did { 30 + return Err(error_response( 31 + StatusCode::FORBIDDEN, 32 + "You can only trigger this for your own account", 33 + )); 34 + } 35 + 36 + let day_five = DayFive { 37 + pool: state.postgres_pool.clone(), 38 + oauth_client: None, 39 + secret_agent: state.secret_agent.clone(), 40 + }; 41 + 42 + day_five 43 + .create_secret_record(&user_did) 44 + .await 45 + .map_err(log_and_respond( 46 + StatusCode::INTERNAL_SERVER_ERROR, 47 + "Error creating the secret record", 48 + ))?; 49 + 50 + Ok(Redirect::to("/day/5")) 51 + }
+164
web/src/handlers/custom/day_six.rs
··· 1 + use crate::AppState; 2 + use atrium_api::types::string::Did; 3 + use atrium_common::resolver::Resolver; 4 + use axum::{ 5 + Json, 6 + extract::State, 7 + http::{HeaderMap, StatusCode}, 8 + response::{IntoResponse, Response}, 9 + }; 10 + use serde_json::json; 11 + use shared::advent::challenges::day_six::{self}; 12 + use shared::atrium::service_auth::{ 13 + decode_and_verify_service_auth, decode_jwt_claims, extract_signing_key_bytes, 14 + }; 15 + 16 + /// XRPC endpoint for Day 6 Part 2: verifies a serviceAuth JWT from the Authorization header 17 + /// and returns the user's verification code. 18 + pub async fn xrpc_handler( 19 + State(state): State<AppState>, 20 + headers: HeaderMap, 21 + ) -> Result<Json<serde_json::Value>, Response> { 22 + let xrpc_error = |status: StatusCode, error: &str, message: &str| -> Response { 23 + (status, Json(json!({"error": error, "message": message}))).into_response() 24 + }; 25 + 26 + // Extract Bearer token from Authorization header 27 + let auth_header = headers 28 + .get("authorization") 29 + .and_then(|v| v.to_str().ok()) 30 + .ok_or_else(|| { 31 + xrpc_error( 32 + StatusCode::UNAUTHORIZED, 33 + "AuthMissing", 34 + "Missing Authorization header", 35 + ) 36 + })?; 37 + 38 + let jwt = auth_header.strip_prefix("Bearer ").ok_or_else(|| { 39 + xrpc_error( 40 + StatusCode::UNAUTHORIZED, 41 + "AuthMissing", 42 + "Authorization header must use Bearer scheme", 43 + ) 44 + })?; 45 + 46 + // Decode claims to get issuer 47 + let claims = decode_jwt_claims(jwt).map_err(|e| { 48 + xrpc_error( 49 + StatusCode::UNAUTHORIZED, 50 + "InvalidToken", 51 + &format!("Could not decode JWT: {e}"), 52 + ) 53 + })?; 54 + 55 + let expected_service_did = format!( 56 + "did:web:{}", 57 + std::env::var("OAUTH_HOST").unwrap_or_default() 58 + ); 59 + 60 + // Check aud 61 + if claims.aud != expected_service_did { 62 + return Err(xrpc_error( 63 + StatusCode::UNAUTHORIZED, 64 + "InvalidToken", 65 + &format!( 66 + "JWT audience '{}' does not match expected '{}'", 67 + claims.aud, expected_service_did 68 + ), 69 + )); 70 + } 71 + 72 + // Check lxm 73 + match &claims.lxm { 74 + Some(lxm) if lxm == day_six::LXM_PART_TWO => {} 75 + Some(lxm) => { 76 + return Err(xrpc_error( 77 + StatusCode::UNAUTHORIZED, 78 + "InvalidToken", 79 + &format!( 80 + "JWT lxm '{}' does not match expected '{}'", 81 + lxm, 82 + day_six::LXM_PART_TWO 83 + ), 84 + )); 85 + } 86 + None => { 87 + return Err(xrpc_error( 88 + StatusCode::UNAUTHORIZED, 89 + "InvalidToken", 90 + "JWT is missing the lxm claim", 91 + )); 92 + } 93 + } 94 + 95 + // Resolve DID document and verify signature 96 + let iss_did: Did = claims.iss.parse().map_err(|_| { 97 + xrpc_error( 98 + StatusCode::BAD_REQUEST, 99 + "InvalidToken", 100 + "Invalid DID in JWT iss claim", 101 + ) 102 + })?; 103 + let did_doc = state 104 + .handle_resolver 105 + .resolve(&iss_did) 106 + .await 107 + .map_err(|e| { 108 + log::error!("Failed to resolve DID for {}: {}", claims.iss, e); 109 + xrpc_error( 110 + StatusCode::INTERNAL_SERVER_ERROR, 111 + "InternalError", 112 + "Failed to resolve DID document", 113 + ) 114 + })?; 115 + 116 + let (key_alg, key_bytes) = extract_signing_key_bytes(&did_doc).map_err(|e| { 117 + xrpc_error( 118 + StatusCode::UNAUTHORIZED, 119 + "InvalidToken", 120 + &format!("Could not extract signing key: {e}"), 121 + ) 122 + })?; 123 + 124 + decode_and_verify_service_auth(jwt, &key_bytes, key_alg).map_err(|e| { 125 + xrpc_error( 126 + StatusCode::UNAUTHORIZED, 127 + "InvalidToken", 128 + &format!("JWT verification failed: {e}"), 129 + ) 130 + })?; 131 + 132 + // Look up the user's day 6 challenge record 133 + let challenge = sqlx::query_as::<_, shared::models::db_models::ChallengeProgress>( 134 + "SELECT * FROM challenges WHERE user_did = $1 AND day = $2", 135 + ) 136 + .bind(&claims.iss) 137 + .bind(6i16) 138 + .fetch_optional(&state.postgres_pool) 139 + .await 140 + .map_err(|e| { 141 + log::error!("DB error looking up day 6 challenge: {}", e); 142 + xrpc_error( 143 + StatusCode::INTERNAL_SERVER_ERROR, 144 + "InternalError", 145 + "Database error", 146 + ) 147 + })?; 148 + 149 + match challenge { 150 + Some(c) => match c.verification_code_two { 151 + Some(code) => Ok(Json(json!({"code": code}))), 152 + None => Err(xrpc_error( 153 + StatusCode::BAD_REQUEST, 154 + "NotReady", 155 + "Part 2 has not been started yet. Complete Part 1 and visit the Day 6 page first.", 156 + )), 157 + }, 158 + None => Err(xrpc_error( 159 + StatusCode::BAD_REQUEST, 160 + "NotReady", 161 + "You haven't started Day 6 yet. Visit the Day 6 page first.", 162 + )), 163 + } 164 + }
+4
web/src/handlers/custom/mod.rs
··· 1 + pub mod day_five; 2 + pub mod day_six; 3 + 4 + use super::log_and_respond;
+10 -203
web/src/handlers/day.rs
··· 3 3 use crate::{AppState, error_response}; 4 4 use atrium_api::agent::Agent; 5 5 use atrium_api::types::string::Did; 6 - use atrium_common::resolver::Resolver; 7 6 use axum::{ 8 - Json, 9 7 extract::{Form, Path, State}, 10 - http::{HeaderMap, StatusCode}, 8 + http::StatusCode, 11 9 response::{IntoResponse, Redirect, Response}, 12 - }; 13 - use serde_json::json; 14 - use shared::advent::challenges::day_five::DayFive; 15 - use shared::advent::challenges::day_four::DayFour; 16 - use shared::advent::challenges::day_six::{self, DaySix}; 17 - use shared::atrium::service_auth::{ 18 - decode_and_verify_service_auth, decode_jwt_claims, extract_signing_key_bytes, 19 10 }; 20 11 use shared::{ 21 12 OAuthAgentType, OAuthClientType, 22 13 advent::ChallengeCheckResponse, 23 - advent::challenges::day_one::DayOne, 24 - advent::challenges::day_three::DayThree, 25 - advent::challenges::day_two::DayTwo, 26 14 advent::day::Day, 27 15 advent::{AdventChallenge, AdventError}, 28 16 advent::{AdventPart, CompletionStatus}, 17 + }; 18 + use shared::advent::challenges::{ 19 + day_one::DayOne, 20 + day_two::DayTwo, 21 + day_three::DayThree, 22 + day_four::DayFour, 23 + day_five::DayFive, 24 + day_six::DaySix, 29 25 }; 30 26 31 27 fn pick_day( ··· 92 88 } 93 89 } 94 90 95 - fn log_and_respond<E: std::fmt::Display>( 91 + pub(super) fn log_and_respond<E: std::fmt::Display>( 96 92 status: StatusCode, 97 93 context: &'static str, 98 94 ) -> impl FnOnce(E) -> Response { ··· 483 479 } 484 480 } 485 481 } 486 - 487 - /// Endpoint for day 5: creates a record on the secret agent's repo with the user's verification code, 488 - /// then redirects back to /day/5 so the user can find the code via firehose/jetstream. 489 - pub async fn day_five_create_record_handler( 490 - Path(user_did): Path<String>, 491 - state: State<AppState>, 492 - session: AxumSessionStore, 493 - ) -> Result<impl IntoResponse, Response> { 494 - // Verify the user is logged in and the DID matches 495 - let session_did = session.get_did().ok_or_else(|| { 496 - error_response( 497 - StatusCode::FORBIDDEN, 498 - "You need to be logged in to do this", 499 - ) 500 - })?; 501 - 502 - if session_did != user_did { 503 - return Err(error_response( 504 - StatusCode::FORBIDDEN, 505 - "You can only trigger this for your own account", 506 - )); 507 - } 508 - 509 - let day_five = DayFive { 510 - pool: state.postgres_pool.clone(), 511 - oauth_client: None, 512 - secret_agent: state.secret_agent.clone(), 513 - }; 514 - 515 - day_five 516 - .create_secret_record(&user_did) 517 - .await 518 - .map_err(log_and_respond( 519 - StatusCode::INTERNAL_SERVER_ERROR, 520 - "Error creating the secret record", 521 - ))?; 522 - 523 - Ok(Redirect::to("/day/5")) 524 - } 525 - 526 - /// XRPC endpoint for Day 6 Part 2: verifies a serviceAuth JWT from the Authorization header 527 - /// and returns the user's verification code. 528 - pub async fn day_six_xrpc_handler( 529 - State(state): State<AppState>, 530 - headers: HeaderMap, 531 - ) -> Result<Json<serde_json::Value>, Response> { 532 - let xrpc_error = |status: StatusCode, error: &str, message: &str| -> Response { 533 - (status, Json(json!({"error": error, "message": message}))).into_response() 534 - }; 535 - 536 - // Extract Bearer token from Authorization header 537 - let auth_header = headers 538 - .get("authorization") 539 - .and_then(|v| v.to_str().ok()) 540 - .ok_or_else(|| { 541 - xrpc_error( 542 - StatusCode::UNAUTHORIZED, 543 - "AuthMissing", 544 - "Missing Authorization header", 545 - ) 546 - })?; 547 - 548 - let jwt = auth_header.strip_prefix("Bearer ").ok_or_else(|| { 549 - xrpc_error( 550 - StatusCode::UNAUTHORIZED, 551 - "AuthMissing", 552 - "Authorization header must use Bearer scheme", 553 - ) 554 - })?; 555 - 556 - // Decode claims to get issuer 557 - let claims = decode_jwt_claims(jwt).map_err(|e| { 558 - xrpc_error( 559 - StatusCode::UNAUTHORIZED, 560 - "InvalidToken", 561 - &format!("Could not decode JWT: {e}"), 562 - ) 563 - })?; 564 - 565 - let expected_service_did = format!( 566 - "did:web:{}", 567 - std::env::var("OAUTH_HOST").unwrap_or_default() 568 - ); 569 - 570 - // Check aud 571 - if claims.aud != expected_service_did { 572 - return Err(xrpc_error( 573 - StatusCode::UNAUTHORIZED, 574 - "InvalidToken", 575 - &format!( 576 - "JWT audience '{}' does not match expected '{}'", 577 - claims.aud, expected_service_did 578 - ), 579 - )); 580 - } 581 - 582 - // Check lxm 583 - match &claims.lxm { 584 - Some(lxm) if lxm == day_six::LXM_PART_TWO => {} 585 - Some(lxm) => { 586 - return Err(xrpc_error( 587 - StatusCode::UNAUTHORIZED, 588 - "InvalidToken", 589 - &format!( 590 - "JWT lxm '{}' does not match expected '{}'", 591 - lxm, 592 - day_six::LXM_PART_TWO 593 - ), 594 - )); 595 - } 596 - None => { 597 - return Err(xrpc_error( 598 - StatusCode::UNAUTHORIZED, 599 - "InvalidToken", 600 - "JWT is missing the lxm claim", 601 - )); 602 - } 603 - } 604 - 605 - // Resolve DID document and verify signature 606 - let iss_did: Did = claims.iss.parse().map_err(|_| { 607 - xrpc_error( 608 - StatusCode::BAD_REQUEST, 609 - "InvalidToken", 610 - "Invalid DID in JWT iss claim", 611 - ) 612 - })?; 613 - let did_doc = state 614 - .handle_resolver 615 - .resolve(&iss_did) 616 - .await 617 - .map_err(|e| { 618 - log::error!("Failed to resolve DID for {}: {}", claims.iss, e); 619 - xrpc_error( 620 - StatusCode::INTERNAL_SERVER_ERROR, 621 - "InternalError", 622 - "Failed to resolve DID document", 623 - ) 624 - })?; 625 - 626 - let (key_alg, key_bytes) = extract_signing_key_bytes(&did_doc).map_err(|e| { 627 - xrpc_error( 628 - StatusCode::UNAUTHORIZED, 629 - "InvalidToken", 630 - &format!("Could not extract signing key: {e}"), 631 - ) 632 - })?; 633 - 634 - decode_and_verify_service_auth(jwt, &key_bytes, key_alg).map_err(|e| { 635 - xrpc_error( 636 - StatusCode::UNAUTHORIZED, 637 - "InvalidToken", 638 - &format!("JWT verification failed: {e}"), 639 - ) 640 - })?; 641 - 642 - // Look up the user's day 6 challenge record 643 - let challenge = sqlx::query_as::<_, shared::models::db_models::ChallengeProgress>( 644 - "SELECT * FROM challenges WHERE user_did = $1 AND day = $2", 645 - ) 646 - .bind(&claims.iss) 647 - .bind(6i16) 648 - .fetch_optional(&state.postgres_pool) 649 - .await 650 - .map_err(|e| { 651 - log::error!("DB error looking up day 6 challenge: {}", e); 652 - xrpc_error( 653 - StatusCode::INTERNAL_SERVER_ERROR, 654 - "InternalError", 655 - "Database error", 656 - ) 657 - })?; 658 - 659 - match challenge { 660 - Some(c) => match c.verification_code_two { 661 - Some(code) => Ok(Json(json!({"code": code}))), 662 - None => Err(xrpc_error( 663 - StatusCode::BAD_REQUEST, 664 - "NotReady", 665 - "Part 2 has not been started yet. Complete Part 1 and visit the Day 6 page first.", 666 - )), 667 - }, 668 - None => Err(xrpc_error( 669 - StatusCode::BAD_REQUEST, 670 - "NotReady", 671 - "You haven't started Day 6 yet. Visit the Day 6 page first.", 672 - )), 673 - } 674 - }
+3
web/src/handlers/mod.rs
··· 1 1 pub mod auth; 2 + pub mod custom; 2 3 pub mod day; 3 4 pub mod did; 4 5 pub mod leaderboard; 5 6 pub mod oauth_metadata; 7 + 8 + use day::log_and_respond;
+2 -2
web/src/main.rs
··· 288 288 ) 289 289 .route( 290 290 "/day/5/{user_did}", 291 - get(handlers::day::day_five_create_record_handler), 291 + get(handlers::custom::day_five::create_record_handler), 292 292 ) 293 293 .route( 294 294 "/xrpc/codes.advent.challenge.getChallengeCode", 295 - get(handlers::day::day_six_xrpc_handler), 295 + get(handlers::custom::day_six::xrpc_handler), 296 296 ) 297 297 .route( 298 298 "/leaderboard",