use serde_json::json; use std::{collections::HashMap, env, str::FromStr, sync::Arc}; use uuid::Uuid; use server::auth::AuthenticatedSession; use axum::{ extract::{Path, Query, State}, response::{Html, Redirect}, routing::{delete, get, post}, Json, Router, }; use protocol::{ bridge_engine::TableStatePlayerView, card::Card, actions::Bid, core::Player }; use protocol::{Table, UserInfo}; use server::server::ServerState; use tower_cookies::{Cookie, CookieManagerLayer, Cookies}; use tower_http::trace::TraceLayer; use tracing::{info, log::warn}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use server::{ auth::{OauthAuthenticator, SessionId, Authenticator}, play::advance_play, server::ServerContext, error::BridgeError, play::{DbJournal, Journal}, }; use sqlx::{postgres::PgPoolOptions, PgPool}; 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(server::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).await } #[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").expect("DATABASE_URL"); 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").expect("BIND_ADDRESS"); info!("Starting server on {}", bind_address); let app_url = env::var("APP_URL").expect("APP_URL"); #[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/table/:id/bid", post(post_bid)) .route("/api/table/:id/play", post(post_play)) .route("/api/table/:id/admin/deal", post(table_new_deal)) .route("/api/login", get(login)) .route(server::auth::LOGIN_CALLBACK, get(login_callback)) .layer(CookieManagerLayer::new()) .layer(TraceLayer::new_for_http()) .with_state(state); axum::Server::bind(&bind_address.parse().unwrap()) .serve(app.into_make_service()) .await .unwrap(); } #[cfg(debug_assertions)] async fn fake_login() -> Html<&'static str> { Html( r#"

Log in as:

"#, ) } async fn get_table_view( _session: AuthenticatedSession, State(state): ServerState, Path(id): Path, ) -> Result, BridgeError> { info!("Getting table state for {id:}"); let player_position = Player::South; let jnl = DbJournal::new(state.db.clone(), id); let mut table = server::play::Table::new_or_replay(jnl).await?; info!("Advancing play"); while table.game_in_progress() && table.game()?.current_player() != player_position // TODO: Support other player configurations. && table.game()?.current_player() != player_position.many_next(2) { advance_play(&mut table).await?; } let response = Json(TableStatePlayerView::from_table_state( &table.state, player_position, )); info!("Response: {response:#?}"); Ok(response) } async fn table_new_deal( _session: AuthenticatedSession, State(state): ServerState, Path(id): Path, ) -> Result, BridgeError> { info!("Getting table state for {id:}"); let jnl = DbJournal::new(state.db.clone(), id); let mut table = server::play::Table::replay(jnl).await?; table.new_deal().await?; Ok(Json(())) } async fn post_bid( _session: AuthenticatedSession, State(state): ServerState, Path(id): Path, Json(bid): Json, ) -> Result, BridgeError> { info!("Getting table state for {id:}"); let jnl = DbJournal::new(state.db.clone(), id); let mut table = server::play::Table::replay(jnl).await?; table.bid(bid).await?; Ok(Json(())) } async fn post_play( _session: AuthenticatedSession, State(state): ServerState, Path(id): Path, Json(card): Json, ) -> Result, BridgeError> { info!("Getting table state for {id:}"); let jnl = DbJournal::new(state.db.clone(), id); let mut table = server::play::Table::replay(jnl).await?; table.play(card).await?; Ok(Json(())) } async fn leave_table( session: AuthenticatedSession, State(state): ServerState, ) -> Result<(), BridgeError> { sqlx::query!( r#" delete from table_players where player_id = $1 "#, session.player_id ) .execute(&state.db) .await?; Ok(()) } async fn create_table( session: AuthenticatedSession, State(state): ServerState, ) -> Result, BridgeError> { let txn = state.db.begin().await?; let table_id = sqlx::query!( r#" insert into bridge_table (id) values ($1) returning id "#, Uuid::new_v4() ) .fetch_one(&state.db) .await? .id; sqlx::query!( r#" insert into table_players (table_id, player_id, position) values ($1, $2, 'south') "#, table_id, session.player_id ) .execute(&state.db) .await?; txn.commit().await?; Ok(Json(table_id)) } async fn user_info( session: Option, State(state): ServerState, ) -> Result>, BridgeError> { let mut session = match session { None => return Ok(Json(None)), Some(s) => s, }; Ok(Json(Some(UserInfo { username: state.authenticator.user_info(&mut session).await?, table: user_table(&*state, &session).await?, }))) } async fn user_table( state: &ServerContext, session: &AuthenticatedSession, ) -> Result, BridgeError> { Ok(sqlx::query_as!( Table, r#" select tables.id from table_players players natural join bridge_table tables where player_id = $1 "#, session.player_id ) .fetch_optional(&state.db) .await?) } async fn login_callback( cookies: Cookies, Query(params): Query>, State(state): ServerState, ) -> Result { let cookie = cookies.get("user-id").unwrap(); let user_id: SessionId = SessionId::from_str(cookie.value())?; let session = state .authenticator .authenticate(&state.db, user_id, params) .await?; info!("Logged in session: {session:?}"); Ok(Redirect::temporary(&state.app_url)) } async fn login(cookies: Cookies, State(state): ServerState) -> Redirect { let (user_id, auth_url) = state.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()) }