this repo has no description
at flatten-challenge-check 236 lines 7.8 kB view raw
1use crate::{ 2 templates::HtmlTemplate, templates::error::ErrorTemplate, templates::home::HomeTemplate, 3}; 4use atrium_identity::{ 5 did::{CommonDidResolver, CommonDidResolverConfig, DEFAULT_PLC_DIRECTORY_URL}, 6 handle::{AtprotoHandleResolver, AtprotoHandleResolverConfig}, 7}; 8use atrium_oauth::{ 9 AtprotoLocalhostClientMetadata, DefaultHttpClient, KnownScope, OAuthClient, OAuthClientConfig, 10 OAuthResolverConfig, Scope, 11}; 12use axum::{ 13 Router, 14 http::StatusCode, 15 middleware, 16 response::IntoResponse, 17 response::Response, 18 routing::{get, post}, 19}; 20use bb8_redis::RedisConnectionManager; 21use chrono::Datelike; 22use dotenv::dotenv; 23use redis::AsyncCommands; 24use shared::{ 25 HandleResolver, OAuthClientType, atrium::dns_resolver::HickoryDnsTxtResolver, 26 atrium::stores::AtriumSessionStore, atrium::stores::AtriumStateStore, 27}; 28use sqlx::{PgPool, postgres::PgPoolOptions}; 29use std::{ 30 env, 31 net::{IpAddr, Ipv4Addr, SocketAddr}, 32 sync::Arc, 33 time, 34}; 35use time::Duration; 36use tower_http::trace::TraceLayer; 37use tower_sessions::{SessionManagerLayer, cookie::SameSite}; 38use tracing_subscriber::EnvFilter; 39 40mod handlers; 41 42extern crate dotenv; 43 44mod extractors; 45mod redis_session_store; 46mod session; 47mod templates; 48mod unlock; 49 50#[derive(Clone)] 51struct AppState { 52 postgres_pool: PgPool, 53 redis_pool: bb8::Pool<RedisConnectionManager>, 54 oauth_client: OAuthClientType, 55 //Used to get did to handle leaving because I figured we'd need it 56 _handle_resolver: HandleResolver, 57} 58 59fn oauth_scopes() -> Vec<Scope> { 60 vec![ 61 Scope::Known(KnownScope::Atproto), 62 //Gives full CRUD to the codes.advent.* collection 63 Scope::Unknown("repo:codes.advent.*".to_string()), 64 ] 65} 66 67fn error_response(status: StatusCode, message: &str) -> Response { 68 IntoResponse::into_response(( 69 status, 70 HtmlTemplate(ErrorTemplate { 71 title: "at://advent - Error", 72 message, 73 }), 74 )) 75} 76 77#[tokio::main] 78async fn main() -> Result<(), Box<dyn std::error::Error>> { 79 dotenv().ok(); 80 81 //Sets up logging/tracing 82 tracing_subscriber::fmt() 83 .with_env_filter( 84 EnvFilter::try_from_default_env() 85 .or_else(|_| EnvFilter::try_new("info,axum_tracing_example=error,tower_http=warn")) 86 .unwrap(), 87 ) 88 .init(); 89 90 let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 7878); 91 let host = addr.ip(); 92 let port = addr.port(); 93 let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); 94 95 //sqlx pool 96 let database_url = 97 env::var("DATABASE_URL").expect("DATABASE_URL must be set in the environment or .env"); 98 99 // set up a postgres connection pool 100 let postgres_pool = PgPoolOptions::new() 101 .max_connections(5) 102 .acquire_timeout(Duration::from_secs(3)) 103 .connect(&database_url) 104 .await 105 .expect("can't connect to database"); 106 107 // redis pool setup 108 let redis_url = 109 env::var("REDIS_URL").expect("REDIS_URL must be set in the environment or .env"); 110 let manager = RedisConnectionManager::new(redis_url.clone()).unwrap(); 111 let redis_pool = bb8::Pool::builder().build(manager).await.unwrap(); 112 //cam be deleted, just an example for the test endpoint 113 { 114 // ping the database before starting 115 let mut conn = redis_pool.get().await.unwrap(); 116 conn.set::<&str, &str, ()>("foo", "bar").await.unwrap(); 117 let result: String = conn.get("foo").await.unwrap(); 118 assert_eq!(result, "bar"); 119 } 120 121 //Atrium/atproto setup 122 123 //Create a new handle resolver for the home page 124 let http_client = Arc::new(DefaultHttpClient::default()); 125 126 let handle_resolver = CommonDidResolver::new(CommonDidResolverConfig { 127 plc_directory_url: DEFAULT_PLC_DIRECTORY_URL.to_string(), 128 http_client: http_client.clone(), 129 }); 130 let handle_resolver = Arc::new(handle_resolver); 131 132 // Create a new OAuth client 133 let http_client = Arc::new(DefaultHttpClient::default()); 134 let config = OAuthClientConfig { 135 client_metadata: AtprotoLocalhostClientMetadata { 136 redirect_uris: Some(vec![String::from(format!( 137 //This must match the endpoint you use the callback function 138 "http://{host}:{port}/oauth/callback" 139 ))]), 140 scopes: Some(oauth_scopes()), 141 }, 142 keys: None, 143 resolver: OAuthResolverConfig { 144 did_resolver: CommonDidResolver::new(CommonDidResolverConfig { 145 plc_directory_url: DEFAULT_PLC_DIRECTORY_URL.to_string(), 146 http_client: http_client.clone(), 147 }), 148 handle_resolver: AtprotoHandleResolver::new(AtprotoHandleResolverConfig { 149 dns_txt_resolver: HickoryDnsTxtResolver::default(), 150 http_client: http_client.clone(), 151 }), 152 authorization_server_metadata: Default::default(), 153 protected_resource_metadata: Default::default(), 154 }, 155 state_store: AtriumStateStore::new(redis_pool.clone()), 156 session_store: AtriumSessionStore::new(redis_pool.clone()), 157 }; 158 let client = Arc::new(OAuthClient::new(config).expect("failed to create OAuth client")); 159 160 let session_store = redis_session_store::RedisSessionStore::new(redis_pool.clone()); 161 let session_layer = SessionManagerLayer::new(session_store) 162 //Set to lax so session id cookie can be set on redirect 163 .with_same_site(SameSite::Lax) 164 .with_secure(false); 165 166 let app_state = AppState { 167 postgres_pool, 168 redis_pool, 169 oauth_client: client, 170 _handle_resolver: handle_resolver, 171 }; 172 //HACK Yeah I don't like it either - bt 173 let prod: bool = env::var("PROD") 174 .map(|val| val == "true") 175 .unwrap_or_else(|_| true); 176 log::info!("listening on http://{}", addr); 177 let app = Router::new() 178 .route("/", get(home_handler)) 179 .route( 180 "/day/{id}", 181 match prod { 182 true => get(handlers::day::view_day_handler) 183 .route_layer(middleware::from_fn(unlock::unlock)), 184 false => get(handlers::day::view_day_handler), 185 }, 186 ) 187 .route( 188 "/day/{id}", 189 match prod { 190 true => post(handlers::day::post_day_handler) 191 .route_layer(middleware::from_fn(unlock::unlock)), 192 false => post(handlers::day::post_day_handler), 193 }, 194 ) 195 .route("/login", get(handlers::auth::login_page_handler)) 196 .route("/handle", get(handlers::auth::handle_root_handler)) 197 .route("/login/{handle}", get(handlers::auth::login_handle)) 198 .route( 199 "/oauth/callback", 200 get(handlers::auth::oauth_callback_handler), 201 ) 202 .layer(session_layer) 203 .with_state(app_state) 204 .layer(TraceLayer::new_for_http()); 205 axum::serve(listener, app).await?; 206 Ok(()) 207} 208 209/// Landing page showing currently unlocked days and a login button 210async fn home_handler() -> impl IntoResponse { 211 //TODO make a helper function for this since it is similar to the middleware 212 let now = chrono::Utc::now(); 213 let mut unlocked: Vec<u8> = Vec::new(); 214 215 //HACK Yeah I don't like it either - bt 216 let prod: bool = env::var("PROD") 217 .map(|val| val == "true") 218 .unwrap_or_else(|_| true); 219 if prod { 220 if now.month() == 12 { 221 let today = now.day().min(25); 222 for d in 1..=today { 223 unlocked.push(d as u8); 224 } 225 } 226 } else { 227 for d in 1..=25 { 228 unlocked.push(d as u8); 229 } 230 } 231 232 HtmlTemplate(HomeTemplate { 233 title: "at://advent", 234 unlocked_days: unlocked, 235 }) 236}