Something went wrong. Try again.
advent of atproto
Something went wrong. Try again.
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237use crate::{ templates::HtmlTemplate, templates::error::ErrorTemplate, templates::home::HomeTemplate,};use atrium_identity::{ did::{CommonDidResolver, CommonDidResolverConfig, DEFAULT_PLC_DIRECTORY_URL}, handle::{AtprotoHandleResolver, AtprotoHandleResolverConfig},};use atrium_oauth::{ AtprotoLocalhostClientMetadata, DefaultHttpClient, KnownScope, OAuthClient, OAuthClientConfig, OAuthResolverConfig, Scope,};use axum::{ Router, http::StatusCode, middleware, response::IntoResponse, response::Response, routing::{get, post},};use bb8_redis::RedisConnectionManager;use chrono::Datelike;use dotenv::dotenv;use redis::AsyncCommands;use shared::{ HandleResolver, OAuthClientType, atrium::dns_resolver::HickoryDnsTxtResolver, atrium::stores::AtriumSessionStore, atrium::stores::AtriumStateStore,};use sqlx::{PgPool, postgres::PgPoolOptions};use std::{ env, net::{IpAddr, Ipv4Addr, SocketAddr}, sync::Arc, time,};use time::Duration;use tower_http::trace::TraceLayer;use tower_sessions::{SessionManagerLayer, cookie::SameSite};use tracing_subscriber::EnvFilter;
mod handlers;
extern crate dotenv;
mod extractors;mod redis_session_store;mod session;mod templates;mod unlock;
#[derive(Clone)]struct AppState { postgres_pool: PgPool, redis_pool: bb8::Pool<RedisConnectionManager>, oauth_client: OAuthClientType, //Used to get did to handle leaving because I figured we'd need it _handle_resolver: HandleResolver,}
fn oauth_scopes() -> Vec<Scope> { vec![ Scope::Known(KnownScope::Atproto), //Gives full CRUD to the codes.advent.* collection Scope::Unknown("repo:codes.advent.*".to_string()), ]}
fn error_response(status: StatusCode, message: &str) -> Response { IntoResponse::into_response(( status, HtmlTemplate(ErrorTemplate { title: "at://advent - Error", message, }), ))}
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { dotenv().ok();
//Sets up logging/tracing tracing_subscriber::fmt() .with_env_filter( EnvFilter::try_from_default_env() .or_else(|_| EnvFilter::try_new("info,axum_tracing_example=error,tower_http=warn")) .unwrap(), ) .init();
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 7878); let host = addr.ip(); let port = addr.port(); let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
//sqlx pool let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set in the environment or .env");
// set up a postgres connection pool let postgres_pool = PgPoolOptions::new() .max_connections(5) .acquire_timeout(Duration::from_secs(3)) .connect(&database_url) .await .expect("can't connect to database");
// redis pool setup let redis_url = env::var("REDIS_URL").expect("REDIS_URL must be set in the environment or .env"); let manager = RedisConnectionManager::new(redis_url.clone()).unwrap(); let redis_pool = bb8::Pool::builder().build(manager).await.unwrap(); //cam be deleted, just an example for the test endpoint { // ping the database before starting let mut conn = redis_pool.get().await.unwrap(); conn.set::<&str, &str, ()>("foo", "bar").await.unwrap(); let result: String = conn.get("foo").await.unwrap(); assert_eq!(result, "bar"); }
//Atrium/atproto setup
//Create a new handle resolver for the home page let http_client = Arc::new(DefaultHttpClient::default());
let handle_resolver = CommonDidResolver::new(CommonDidResolverConfig { plc_directory_url: DEFAULT_PLC_DIRECTORY_URL.to_string(), http_client: http_client.clone(), }); let handle_resolver = Arc::new(handle_resolver);
// Create a new OAuth client let http_client = Arc::new(DefaultHttpClient::default()); let config = OAuthClientConfig { client_metadata: AtprotoLocalhostClientMetadata { redirect_uris: Some(vec![String::from(format!( //This must match the endpoint you use the callback function "http://{host}:{port}/oauth/callback" ))]), scopes: Some(oauth_scopes()), }, keys: None, resolver: OAuthResolverConfig { did_resolver: CommonDidResolver::new(CommonDidResolverConfig { plc_directory_url: DEFAULT_PLC_DIRECTORY_URL.to_string(), http_client: http_client.clone(), }), handle_resolver: AtprotoHandleResolver::new(AtprotoHandleResolverConfig { dns_txt_resolver: HickoryDnsTxtResolver::default(), http_client: http_client.clone(), }), authorization_server_metadata: Default::default(), protected_resource_metadata: Default::default(), }, state_store: AtriumStateStore::new(redis_pool.clone()), session_store: AtriumSessionStore::new(redis_pool.clone()), }; let client = Arc::new(OAuthClient::new(config).expect("failed to create OAuth client"));
let session_store = redis_session_store::RedisSessionStore::new(redis_pool.clone()); let session_layer = SessionManagerLayer::new(session_store) //Set to lax so session id cookie can be set on redirect .with_same_site(SameSite::Lax) .with_secure(false);
let app_state = AppState { postgres_pool, redis_pool, oauth_client: client, _handle_resolver: handle_resolver, }; //HACK Yeah I don't like it either - bt let prod: bool = env::var("PROD") .map(|val| val == "true") .unwrap_or_else(|_| true); log::info!("listening on http://{}", addr); let app = Router::new() .route("/", get(home_handler)) .route( "/day/{id}", match prod { true => get(handlers::day::view_day_handler) .route_layer(middleware::from_fn(unlock::unlock)), false => get(handlers::day::view_day_handler), }, ) .route( "/day/{id}", match prod { true => post(handlers::day::post_day_handler) .route_layer(middleware::from_fn(unlock::unlock)), false => post(handlers::day::post_day_handler), }, ) .route("/login", get(handlers::auth::login_page_handler)) .route("/handle", get(handlers::auth::handle_root_handler)) .route("/login/{handle}", get(handlers::auth::login_handle)) .route( "/oauth/callback", get(handlers::auth::oauth_callback_handler), ) .layer(session_layer) .with_state(app_state) .layer(TraceLayer::new_for_http()); axum::serve(listener, app).await?; Ok(())}
/// Landing page showing currently unlocked days and a login buttonasync fn home_handler() -> impl IntoResponse { //TODO make a helper function for this since it is similar to the middleware let now = chrono::Utc::now(); let mut unlocked: Vec<u8> = Vec::new();
//HACK Yeah I don't like it either - bt let prod: bool = env::var("PROD") .map(|val| val == "true") .unwrap_or_else(|_| true); if prod { if now.month() == 12 { let today = now.day().min(25); for d in 1..=today { unlocked.push(d as u8); } } } else { for d in 1..=25 { unlocked.push(d as u8); } }
HtmlTemplate(HomeTemplate { title: "at://advent", unlocked_days: unlocked, })}