use serde_json::json; use std::{collections::HashMap, env, str::FromStr, sync::Arc}; use uuid::Uuid; use auth::AuthenticatedSession; use axum::{ extract::{Extension, Path, Query}, response::{Html, Redirect}, routing::{delete, get, post}, Json, Router, }; use protocol::{Table, UserInfo}; use protocol::bridge_engine::{GameStatePlayerView, Player}; use server::ContextExtension; use tower_cookies::{Cookie, CookieManagerLayer, Cookies}; use tower_http::trace::TraceLayer; use tracing::{info, log::warn}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; mod auth; mod error; mod play; mod server; #[cfg(debug_assertions)] mod fake_auth; use crate::{ auth::{OauthAuthenticator, SessionId}, server::ServerContext, }; use crate::{ error::BridgeError, play::{DbJournal, Journal}, }; use sqlx::{postgres::PgPoolOptions, PgPool}; use auth::Authenticator; async fn create_default_authenticator(db_pool: &PgPool) -> Box { Box::new(OauthAuthenticator::from_env(db_pool.clone()).await) } #[cfg(debug_assertions)] async fn create_authenticator(db_pool: &PgPool) -> Box { const FAKE_AUTHENTICATOR: &str = "fake"; if std::env::var("AUTHENTICATOR").unwrap_or("".to_string()) == FAKE_AUTHENTICATOR { Box::new(fake_auth::FakeAuthenticator::new()) } else { create_default_authenticator(db_pool).await } } #[cfg(not(debug_assertions))] async fn create_authenticator(db_pool: &PgPool) -> Box { create_default_authenticator(db_pool) } #[tokio::main] async fn main() { dotenv::dotenv().ok(); tracing_subscriber::registry() .with(tracing_subscriber::EnvFilter::new( std::env::var("RUST_LOG").unwrap_or_else(|_| "".into()), )) .with(tracing_subscriber::fmt::layer()) .init(); info!("Opening database connection"); let db_url = env::var("DATABASE_URL").unwrap(); let db_pool: PgPool = PgPoolOptions::new() .max_connections(10) .connect(&db_url) .await .expect("db connection"); info!("Running db migrations"); sqlx::migrate!().run(&db_pool).await.expect("db migration"); let mut jnl = DbJournal::new(db_pool.clone(), Uuid::new_v4()); jnl.append(0, json!("starting server")) .await .expect("new object"); let bind_address = env::var("BIND_ADDRESS").unwrap(); info!("Starting server on {}", bind_address); let app_url = env::var("APP_URL").unwrap(); #[cfg(debug_assertions)] warn!("Running a debug build."); let state = Arc::new(ServerContext { app_url, authenticator: create_authenticator(&db_pool).await, db: db_pool, }); let app = Router::new(); #[cfg(debug_assertions)] let app = app .route("/api/fake_login", get(fake_login)); let app = app .route("/api/user/info", get(user_info)) .route("/api/table", post(create_table)) .route("/api/table", delete(leave_table)) .route("/api/table/:id", get(get_table_view)) // .route("/api/user/table", get(user_table)) .route("/api/login", get(login)) .route(auth::LOGIN_CALLBACK, get(login_callback)) .layer(CookieManagerLayer::new()) .layer(Extension(state)) .layer(TraceLayer::new_for_http()); axum::Server::bind(&bind_address.parse().unwrap()) .serve(app.into_make_service()) .await .unwrap(); } #[cfg(debug_assertions)] async fn fake_login() -> Html<&'static str> { use axum::response::Html; Html(r#"

Log in as:

"#) } async fn get_table_view( _session: AuthenticatedSession, extension: ContextExtension, Path(id): Path, ) -> Result, BridgeError> { info!("Getting table state for {id:}"); let jnl = DbJournal::new(extension.db.clone(), id); let table = play::Table::new_or_replay(jnl).await?; let response = Json(GameStatePlayerView::from_game_state(table.game(), Player::South)); info!("Response: {response:#?}"); Ok(response) } async fn leave_table( session: AuthenticatedSession, extension: ContextExtension, ) -> Result<(), BridgeError> { sqlx::query!( r#" delete from table_players where player_id = $1 "#, session.player_id ) .execute(&extension.db) .await?; Ok(()) } async fn create_table( session: AuthenticatedSession, extension: ContextExtension, ) -> Result, BridgeError> { let txn = extension.db.begin().await?; let table_id = sqlx::query!( r#" insert into active_tables (id) values ($1) returning id "#, Uuid::new_v4() ) .fetch_one(&extension.db) .await? .id; sqlx::query!( r#" insert into table_players (active_tables_id, player_id, position) values ($1, $2, 'south') "#, table_id, session.player_id ) .execute(&extension.db) .await?; txn.commit().await?; Ok(Json(table_id)) } async fn user_info( session: Option, extension: ContextExtension, ) -> Result>, BridgeError> { let mut session = match session { None => return Ok(Json(None)), Some(s) => s, }; Ok(Json(Some(UserInfo { username: extension.authenticator.user_info(&mut session).await?, table: user_table(extension, &session).await?, }))) } async fn user_table( extension: ContextExtension, session: &AuthenticatedSession, ) -> Result, BridgeError> { Ok(sqlx::query_as!( Table, r#" select tables.id from table_players players natural join active_tables tables where player_id = $1 "#, session.player_id ) .fetch_optional(&extension.db) .await?) } async fn login_callback( cookies: Cookies, Query(params): Query>, extension: ContextExtension, ) -> Result { let cookie = cookies.get("user-id").unwrap(); let user_id: SessionId = SessionId::from_str(cookie.value())?; let session = extension .authenticator .authenticate(&extension.db, user_id, params) .await?; info!("Logged in session: {session:?}"); Ok(Redirect::temporary(&extension.app_url)) } async fn login(cookies: Cookies, extension: ContextExtension) -> Redirect { let (user_id, auth_url) = extension.authenticator.get_login_url().await; info!("Creating auth url for {user_id:?}"); let user_id = serde_json::to_string(&user_id).unwrap(); let mut cookie = Cookie::new("user-id", user_id.to_string()); cookie.set_http_only(true); cookie.set_secure(true); cookie.set_same_site(cookie::SameSite::Lax); cookies.add(cookie); Redirect::temporary(auth_url.as_str()) }