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,
})
}