use 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, oauth_client: OAuthClientType, //Used to get did to handle leaving because I figured we'd need it _handle_resolver: HandleResolver, } fn oauth_scopes() -> Vec { 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> { 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 button async 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 = 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, }) }